我想知道在Java中,将变量声明为volatile和总是在synchronized(this)块中访问变量之间的区别?

根据本文的第http://www.javamex.com/tutorials/synchronization_volatile.shtml条,有很多要说的,既有很多不同之处,也有一些相似之处.

我特别感兴趣的是这一条信息:

...

  • 对易失性变量的访问永远不会被阻塞:我们只进行简单的读或写操作,因此与同步块不同,我们永远不会持有任何锁;
  • 因为访问易失性变量从来不会持有锁,所以它不适用于我们希望将read-update-write作为原子操作的情况(除非我们准备好"错过更新");

read-update-write是什么意思?写入不也是一种更新吗?或者它们只是意味着update是一种依赖于读取的写入吗?

最重要的是,什么时候声明变量volatile比通过synchronized块访问变量更合适?对依赖于输入的变量使用volatile是个好主意吗?例如,有一个名为render的变量,通过渲染循环读取,并由按键事件设置?

推荐答案

了解线程安全有two个方面是很重要的.

  1. 执行控制,以及
  2. 内存可见性

第一个问题涉及控制代码何时执行(包括指令的执行顺序)以及它是否可以并发执行,第二个问题涉及其他线程何时可以看到内存中已完成的操作的效果.因 for each CPU与主存之间都有几个级别的缓存,所以在不同CPU或内核上运行的线程在任何给定时刻都可以看到不同的"内存",因为线程被允许获取主存的私有副本并在其上工作.

使用synchronized防止任何其他线程获得监视器(或锁)for the same object,从而防止由同步on the same object保护的所有代码块同时执行.同步also创建一个"之前发生"的内存屏障,导致内存可见性约束,使得在某个线程将锁appears释放给另一个随后获取the same lock的线程之前所做的任何事情都发生在该线程获取锁之前.实际上,在当前的硬件上,这通常会导致在获取监控器时刷新CPU缓存,并在释放监控器时写入主内存,这两者都(相对)昂贵.

另一方面,使用volatile会强制对易失性变量的所有访问(读或写)发生在主内存中,从而有效地将易失性变量从CPU缓存中清除.这对于一些只要求变量的可见性正确且访问顺序不重要的操作非常有用.使用volatile还改变了对longdouble的处理,要求访问它们是原子的;在某些(较旧的)硬件上,这可能需要锁,但在现代64位硬件上不需要锁.在Java 5+的新(JSR-133)内存模型下,volatile的语义得到了增强,在内存可见性和指令顺序方面几乎与synchronized一样强大(参见http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile).为了提高可视性,对易失性字段的每次访问都相当于半个同步.

在新的记忆模型下,易变变量之间仍然不能重新排序.不同之处在于,现在对它们周围的正常字段访问进行重新排序不再那么容易.写入易失性字段具有与监视器释放相同的记忆效果,从易失性字段读取具有与监视器获取相同的记忆效果.实际上,由于新的内存模型对易失性字段访问与其他字段访问(无论是否易失性)的重新排序施加了更严格的限制,因此线程A在写入易失性字段f时可见的任何内容在线程B读取f时都变为可见.

--JSR 133 (Java Memory Model) FAQ

因此,现在这两种形式的内存障碍(在当前的JMM下)都会导致指令重新排序障碍,这会阻止编译器或运行时跨该障碍重新排序指令.在旧的JMM中,volatile并没有阻止重新排序.这可能很重要,因为除了内存障碍之外,唯一的限制是,for any particular thread,代码的净效果与指令在源代码中的显示顺序相同.

volatile的一个用途是动态地重新创建共享但不可变的对象,许多其他线程在其执行周期的特定点引用该对象.一个线程需要其他线程在重新创建的对象发布后开始使用该对象,但不需要完全同步的额外开销以及它的伴随争用和缓存刷新.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

特别是在回答你的读-更新-写问题时.请考虑以下不安全代码:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

现在,由于updateCounter()方法不同步,两个线程可以同时进入它.在可能发生的许多排列中,一种是线程1执行计数器==1000的测试,并发现它为真,然后挂起.然后,线程2执行相同的测试,并且也看到它为真,因此被挂起.则线程1恢复并将计数器设置为0.然后线程2恢复并再次将计数器设置为0,因为它错过了来自线程1的更新.即使没有像我所描述的那样发生线程切换,也可能会发生这种情况,只是因为计数器的两个不同的缓存副本存在于两个不同的CPU核心中,并且每个线程都在单独的核心上运行.因此,一个线程可以将计数器设置为一个值,而另一个线程可以将计数器设置为某个完全不同的值,这仅仅是因为缓存的缘故.

在本例中,重要的是变量counter从主存读入高速缓存,在高速缓存中更新,并且仅在稍后出现内存障碍或其他事情需要高速缓存时的某个不确定点写入主存.使计数器volatile不足以保证该代码的线程安全,因为对最大值和赋值的测试是离散操作,包括增量,这是一组非原子read+increment+write机器指令,类似于:

MOV EAX,counter
INC EAX
MOV counter,EAX

只有当对易失性变量执行的all个操作是"原子的"时,它们才有用,例如我的示例中,对完全形成的对象的引用只被读取或写入(实际上,通常只从一个点写入).另一个例子是支持写时副本列表的易失性数组引用,前提是仅通过首先获取引用的本地副本来读取该array.

Java相关问答推荐

如果它最终将被转换为int类型,为什么我们在Java中需要较小的integer类型?

Spring安全实现多个SQL表身份验证

虚拟线程似乎在外部服务调用时阻止运营商线程

Mongo DB Bson和Java:在子文档中添加和返回仅存在于父文档中的字段?

JPackaged应用程序启动MSI调试,然后启动System. exit()

Select 按位运算序列

获取字符串中带空格的数字和Java中的字符

第二次按下按钮后,我需要将按钮恢复到其原始状态,以便它可以再次工作

通过Spring Security公开Spring Boot执行器端点

Kotlin Val是否提供了与Java最终版相同的可见性保证?

使用Class.this.field=Value初始化构造函数中的最后一个字段会产生错误,而使用this.field=Value则不会

使用正则表达式从字符串中提取多个值

如何在透视表中添加对计数列的筛选?

错误:不兼容的类型:Double不能转换为Float

如何在SWT菜单项文本中保留@字符

谷歌应用引擎本地服务器赢得';t在eclipse上运行

java21预览未命名的符号用于try-with-resources

简化每个元素本身都是 map 列表的列表

在JSON上获取反斜杠

ExecutorService:如果我向Executor提交了太多任务,会发生什么?