作者:绫萱
volatile常常用于修饰多线程共享变量,用来保证该变量的可见性。volatile的语意:某个写线程对volatile变量的写入马上可以被后续的某个读线程“看”到。
volatile保证可见性的原理:volatile是通过在编译器生成字节码时,在对volatile变量进行读写指令序列的前后加入内存屏障,来禁止一些处理器重排序保证写入一定发生在读之前的这种happen-before关系。
简单理解:在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个线程本地内存中;以后再取变量值时,就直接从本地内存中取值;当变量值在本线程里改变时,会同时把变量的新值copy到本地内存中,以便保持一致;在某个特定的时候,将本地内存的更改写到系统主内存中去;当变量在因别的线程等而改变了值,并且该变化没有写到系统主内存,本次线程的本地内存中的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致;但是当变量被volatile修饰后,每次更改该变量的时候会将更改结果写到系统主内存中,利用多处理器的缓存一致性,其他处理器会发现自己的缓存行对应的内存地址被修改,就会将自己处理器的缓存行设置为失效,并强制从系统主内存获取最新的数据。这样就能保证即使在别的线程中改变了该变量的值,在本线程中也能取到最新更改后的值。 ConcurrentHashMap之所以有较好的并发性是因为ConcurrentHashMap是无锁读和加锁写,并且利用了分段锁(不是在所有的entry上加锁,而是在一部分entry上加锁)。
那ConcurrentHashMap是怎么实现无锁读的呢?
这是在jdk1.6.0中的读的实现。
不得不赞叹,作者深厚的功底啊,当执行读的时候,先判断count,count就是一个Segment(充当锁的角色)所守护HashEntry的数量。
这里的count是被volatile修饰的。当对这段表的结构进行更改时,在退出前都会去更改count。由于volatile的语意:某个写线程对volatile变量的写入马上可以被后续的某个读线程“看”到,所以这里对count的读一定发生在对count写之后,获得是最新的count。在无锁读的方法中,首先去读取这个最近的count,保证了在执行无锁读的时候表的结构没有被改变。(利用了volatile变量写读的happen-before关系)。 同时当把value设置为volatile时,其他线程所做的改变就能马上被当前线程感知。这样就能支持多个线程并发读了~ 不过我们也知道volatile并不能保证线程安全,它是轻量级的synchronized。 要使 volatile变量提供理想的线程安全,必须同时满足下面两个条件: ● 对变量的写操作不依赖于当前值。 ● 该变量没有包含在具有其他变量的不变式中。 举例:线程安全计数器的自增操作,其实是由3个操作读取-修改-写入操作序列组成的组合操作,volatile不能保证原子性,不能保证在操作期间该变量的值不会改变。
其实这是一种常见的volatile的利用场景——开销较低的读-写锁策略。如果读操作远远超过写操作,您可以结合使用内部锁和 volatile变量来减少公共代码路径的开销。这样读操作只是volatile读操作,性能优于一个无竞争的锁获取的开销。但是当需要对该变量执行写操作,应该加锁。 PS:这里ConcurrentHashMap也有加锁读的情况。利用方法 V readValueUnderLock(HashEntry<K,V> e)。只有value为空的时候,才会加锁读,这种情况就是编译器对value的赋值操作进行重排序了。 感谢家纯师兄的订正和指导。