JAVA并发编程(三)-volatile关键字

Scroll Down

JAVA并发编程(三)-volatile关键字

volatile关键字称为轻量级锁,其作用与锁的作用有相同的地方:保证可见性和有序性.所不同的是,在原子性上它仅能保障写volatile变量操作的原子性,但没有锁的排他性.volatile关键字的使用不会引起上下文切换,因此volatile更像是一个轻量级简易锁.

volatile的作用

volatile的作用包括:保障可见性,保障有序性,保障long/double类型读写的原子性

volatile仅仅保障对其修饰的变量的写操作(以及读操作)本身的原子性,但是不表示对volatile变量的赋值操作一定具有原子性.

count1 = count2 + 1;

这种就不属于原子操作,有可能其他线程更新count2的值,此时赋值给count1就不是原子性的操作.

对volatile变量的赋值操作,右边表达式中只要涉及共享变量,那么这个赋值操作就不是原子操作,碰到这种操作的情况下,我们仍然需要使用锁.

volatile Map map = new HashMap();

objRef = allcate(HashMap.class)//分配对象所需操作空间
invokeConstrucotr(objRef);//初始化objRef引用的变量
map=objRef;//将对象写入变量map

volatile只能保障变量本身赋值的原子性,即第三步,但是这段示例代码中并未涉及到共享变量,所以这个操作也是原子的.

volatile如果需要保障原子性,则赋值操作不能涉及任何共享变量的访问.

写线程对volatile变量的写操作会产生类似于释放锁的效果,读线程对volatile变量的读操作具有获得锁的效果,因此volatile具有保障性和可见性的作用.

对于volatile变量的写操作,JAVA虚拟机会在该操作之前插入一个释放屏障,并在该操作之后插入一个存储屏障.其中释放屏障禁止了volatile写操作与该操作之前任何的读写操作进行重排序,从而保证了volatile写操作之前的任何读、写操作都会先于volatile写操作提交,即其他线程看到写线程对volatile变量更新时,写线程在更新volatile变量之前所执行的任何操作对于读线程也是必然可见的,保障了读线程对写线程在更新volatile白娘前对锁执行的更新操作感知顺序与源代码顺序一致,即保障了有序性.

对于volatile的读操作,JAVA虚拟机会在该操作前插入一个加载屏障,并在该操作之后插入一个获取屏障,加载屏障通过冲刷处理器缓存,使其执行线程的处理器将其他处理器对共享变量所做额更新同步到该处理器的高速缓存中.读线程的执行加载屏障与写线程的执行存储屏障即保证了线程的有序性可见性.

volatile也会给JIT编译器一个提示,使JIT编译器不会对相应代码进行一些优化从而导致可见性的问题.

所以根据以上总结:

  • 写volatile变量与该操作之前的任何读、写操作都不会重排序
  • 读volatile变量与该操作之后的任何读、写操作都不会重排序

volatile对数据类型的使用

volatile关键字只能对数据本身的引用其作用,无法对数据元素的操作起作用(读取、更新数组元素),如果需要对数组元素的读写操作也要起作用,那么我们需要使用AtomicIntegerArray,AtomicReferenceArray,AtomicLongArray类

volatile变量的开销

volatile变量开销包括读变量和写变量两个方面,但是volatile读和写变量都不会导致上下文切换,因此volatile的开销会比锁要小,而且写一个volatile变量会使该操作以及该操作之前的所有动作都对其他处理器同步,因此volatile变量写操作成本介于普通变量的写操作和临界区内写操作之间.而读取volatile变量的成本也比临界区中读取变量要低,因为没有锁的申请释放以及上下文切换的开销,但是其成本可能比读取普通变量更高,因为volatile变量每次读取都需要从高速缓存或者主内存中读取,而不能从寄存器中读取.

1628597-20190312183918006-1043426980

volatile的使用场景

状态标志

使用volatile作为状态标志,应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的一句,此时可以使用volatile变量,好处是使用volatile变量能够通知另外一个线程发生某些事件,而这些线程不需要使用锁就可以获知

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }

保障可见性

使用volatile保障可见性,多个线程共享一个可变状态变量,其中一个线程更新了该变量之后,其他线程无需加锁即可获得最新值

使用volatile代替锁

多个线程共享一组可变状态变量时,可以把这一组变量封装为一个对象,那么对这些状态变量的更新操作可以通过创建一个新的对象并将该对象引用赋值给对应引用型变量来实现,从而避免了锁的使用

使用volatile实现读写锁

使用volatile关键字读线程一定能读取到最新值,对写操作进行加锁,可以达到简易的读写锁的功能.

public class Counter {
    private volatile long count;

    public long vaule() {
        return count;
    }
    public synchronized void increment() {
        count++;
    }
}

其中synchronized保障了同时只能一个线程对该变量进行更新,而volatile保障了count变量的可见性.