Java并发编程(四)可见性

除了使用synchronized关键字用于实现原子性或者确定"临界区(Critical Section)",还有一个重要的方面就是:内存的可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改状态,而且希望确保当一个线程修改了某个对象的状态后,其他线程能够看到状态变化。如果没有同步,那么这种情况也无法实现。

可见性

在单线程环境中,如果向某个变量先写入了值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。

然而读操作和写操作在不同的线程中执行时,情况却并非如此。

通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值。为了确保多个线程之间堆内存写入操作的可见性,必须使用同步机制。

只要在某个线程中无法检测到重排序情况(即使其他线程中可以很明显地看到线程中的排序),那么就无法确保线程中的操作按照程序指定的顺序来执行。

在没有同步的情况下,编译器、处理器以及运行时都有可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

失效的数据可能导致输出错误的值,或者使程序无法结束。如果对象的引用(例如链表的指针)失效,那么情况会更加复杂。失效的数据还可能导致一些令人困惑的故障,例如意料之外的异常、被破坏的数据结构、不精确的计算以及无限循环。

当线程在没有同步的情况下读取变量时,可能会得到一个失效的值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全保证也被称为最低安全性(out-of-thin-air safety)。

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。Java内存模型要求,变量的读取操作和写入操作必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分界成为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很有可能会读取到某个值的高32位和另一个值的低32位。

加锁和可见性

内置锁可以确保某个线程以一种可预测的方式来查看另一个线程的执行结果,如下图所示。当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,这种情况下可以保证,在锁被释放之前,A看到的变量值在B获得锁后同样可以由B看到。换句话说,当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。如果没有同步,那么就无法实现上述保证。

image

现在我们可以理解了为什么在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说是可见的。否则如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能是一个失效值。

volatile变量

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

另一个理解volatile的有效方法就是,把对volatile进行读写的操作都当成是同步的get和set方法。但是volatile的好处是,访问volatile变量不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile是一种比synchronized更轻量级的同步机制。

(在这里终于把volatile关键字理解了,泪目了)

volatile变量对可见性的影响比volatile变量本身更为重要。当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的。因此,从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。(不建议过度依赖volatile提供的可见性,这种方式比同步更脆弱,更加难以理解。)

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)。

调试小提示:对于服务器应用程序,无论在开发阶段还是在测试阶段,当启动JVM时一定要指定-server命令行选项。server模式的JVM将比client模式的JVM进行更多的优化,例如将循环中未被修改的变量提升到循环外部,因此在开发环境中能正确运行的代码,可能会在部署环境(server模式的JVM)中运行失败。

虽然volatile变量很方便,但也存在一些局限性。volatile变量通常用作某个操作完成、发生中断或者状态的标志。尽管volatile变量也可以用于表示其他的状态信息,但在使用时要非常小心。例如,volatile的语义不足以确保递增操作(count++)的原子性。

当满足以下所有条件时,才应该使用volitile变量:

  • 对变量的写入操作不依赖变量当前的值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中。
  • 访问变量时不需要加锁。

感觉还要看一下JVM指令重排

原文地址:https://www.cnblogs.com/tuhooo/p/7921213.html