Java 内存模型和 volatile、final 等关键字
在读写共享对象的时候,Java不保证linearizability
以及sequential consistency
。原因是遵守它们会很大程度的违反编译器的各种优化策略,比如寄存器的分配、冗余读操作的消除等等,这些优化都是基于编译器的重排序优化。在单线程程序中,编译器对指令的重排序是透明的,不需要太关心,但是在多线程程序中,一个线程可能会收到另一个线程的指令重排序的影响,产生意料之外的结果,大部分是错误的。
为了解决这个问题,Java Memory Model定了一些规范,只要程序满足这些规则,就满足sequential consistency
。这些规则非常的复杂,本文重点关注一些直观的规则,可以满足绝大部分场景。
我们先来看一段代码,是经典的double checked locking
模式。
这是一个经典的并发单例的代码,但是在JDK5之前是有bug的。我们先分析一下代码的逻辑。首先我们跳过第一个check,看到synchronized,因为是多线程下的单例,需要使用同步原语synchronized
来避免多个线程同时看到instance
为null
然后创建了多个instance
。回头开头的第一个check,这是一个优化,因为当instance已经创建好了之后,就可以直接返回创建好的单例了,没有必要多线程竞争做第二个check。
看上去一切都好,但是有一个隐藏了很深的问题,举例来说明:
线程A注意到instance为空,尝试获取class的锁,并成功,开始初始化单例。
在Java中,如果构造函数被内联了,共享变量会在空间分配好之后立刻被更新,这时内联的构造函数还没有执行完,但是instance已经不为空了。
线程B此时会注意到instance已经不为空了,这里有两种常见的错误场景
此时instance没有初始化完成,但是因为instance已经不为null了,线程B会认为instance已经初始化完成,直接将其返回。
此时instance已经初始化完成,但是instance还没有扩散到线程B使用的内存(cache coherence)。
在Java的内存模型中,并发场景中的对象都保存在共享内存中,同时在每个线程有一个私有的工作内存,在每个工作内存中保存着对象的cache。在没有显式的同步操作的前提下,线程对自己的缓存区域写操作,不会立刻同步到所有的cache中,线程对对象的读操作,也不会将最新的值同步到本地cache。很多时候你会感到JVM保持了cache的更新,但是实际上JVM并没有责任这样做。所以我们只能保证线程自己的读写,在该线程的视角下是有顺序的。
一般语境下,synchronized
表示原子操作或者互斥等同步术语,在Java中,它还意味着一个线程的cache和共享内存的同步。有些同步事件会让线程将cache写回到共享内存,让其他线程可以及时看到变量值的更新,有些同步事件会让一个线程将自己的cache失效,强制去读共享内存中的最新值。同步事件满足linearizable
,它们是全局有序的,并且每个线程都遵守这个顺序。下面我们介绍一下几个常见的同步事件。
锁和synchronized
代码块
线程可以通过两种方式达到互斥的效果:一种是synchronized
代码块或synchronized
方法,这种会获取一个隐式的锁;另一种就是显式的锁,比如ReentrantLock
等。这种方法效果是一样的。
如果对一个成员的访问都是通过同一把锁,那么这些读写访问满足linearizable
。当一个线程释放锁的时候,它修改过的成员,会将最新的value同步到共享内存中;当一个线程获取到锁的时候,它会让自己的工作内存也就是cache失效,确保从共享内存中读到最新的value。
Volatile
成员
volatile
成员满足linearizable
。读一个volatile
成员就像获取一把锁:线程的工作区失效,成员的最新value被线程读进来;写一个volatile
成员就像释放一把锁:立刻将最新值同步到共享内存。
它和锁的区别在于读写不是原子操作。举一个例子,加入我们有一个volatile
成员x
,那么多个线程执行x++
这个语句,并不能保证线程之间不产生竞争。volatile
一个常见的使用场景就是多个线程对一个成员读,只有一个成员会对这个线程写。其实单例就是这样一个场景,只有第一个获取到锁的线程执行到了单例的初始化,其余的线程都是读取单例。
java.util.concurrent.atomic
包包含一些满足linearizable
的类型,比如AtomicReference<T>
或者AtomicInteger
,它们的compareAndSet()
和set()
方法的行为和volatile
写类似,而get()
方法和volatile
读类似。
Final
成员
final
成员一但初始化后就不可修改了,当final
成员在构造函数中初始化时,需要遵守一定的规则,就可以保证final
成员被所有的线程看到正确的value。例如下面的代码片段:
因为x
是final
成员,所以任何一个线程访问reader()
中可以确保x
的值一定是3,但是y
不一定是4
但是,如非必要,不要在构造函数中将this
传给其他函数,否则其他线程通过this
访问final
成员的时候,final
成员初始化的值可能还没有被同步到共享内存中。
版权声明: 本文为 InfoQ 作者【麻瓜镇】的原创文章。
原文链接:【http://xie.infoq.cn/article/8430e4f592f42bffd70e66a0d】。文章转载请联系作者。
评论