并发编程之volatile

2019-02-14  本文已影响0人  林千景

写在前面

前面一章我们讲了了java原子性的相关概念和知识点,介绍了用于共享变量线程隔离的ThreadLocal,也知道了synchronized是一个重量级的锁,而我们今天要讲的volatile则是轻量级的synchronized,主要是因为它不会引起线程上下文的切换。在讲volatile到底是什么,它能够解决什么样的问题之前,首先不得不提一下Java的内存模型。

JMM内存模型

在Java中,所有实例域,静态域和数组对象都存在于堆中,是所有线程共享的。局部变量,方法参数和异常处理参数不是线程共享,不会存在内存可见性问题。

Java线程之间的通信由Java(简称JMM)内存模型来控制,它决定了对一个变量的写入何时对另一个线程可见。JMM规定了所有的共享变量都存在于主内存中,而每条线程又有自己的工作内存,工作内存内保存了对于主内存中共享变量的拷贝。我们对一个变量的读写是在工作内存中完成的,同时线程间的通信是由工作内存将修改的变量刷新到主内存来进行传递的。下面是JMM的抽象示意图:

01.png

从图上看,线程A和B进行通信的话分成两步:

由于线程B每次都是从主内存拿变量,并不能实时地获取线程A修改的变量值,可能读取的是之前的值,从而出现脏读,这就是不满足可见性的问题。

可见性问题

可见性是指当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,另一个线程立马能够读取到修改后的值。

举个栗子:

//线程1
int i = 0;
i = 10;
//线程2
j=i;

画个图:

02.png

由图可知,假如线程A,B按照这种时间顺序执行的话,j最后的值是0。这就是可见性问题,线程A对变量i修改的值,没有立即对线程B可见。

有序性问题

有序性:即程序执行的顺序按照代码的先后顺序执行。

举个栗子:

public class VolatileDemo {
    private static boolean flag;
    private static int num;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
                while (!flag) {
                    Thread.yield();
                }
                System.out.println(num);
        });
        t1.start();
        num = 5;
        flag = true;
    }
}

这段代码,尽管num=5是写在flag=true前面,但是最终打印的结果有可能是0哦。也就是说,写在前面的代码并没有先执行,对于这种不按书写顺序执行的情况称作指令重排序。大多数现代处理器都支持指令重排,为的是直接运行当前能运行的指令,而不去顺序等待,这种乱序的执行方式打打提高了处理器的效率。

戏说不是胡说,改编不是乱编,指令重排也不是随便排,它是根据代码的依赖关系,在不影响单线程环境下的执行结果的前提下进行重排序的。例如:

a=1;
b=2;
c=a+b;

这段代码,c=a+b是不会重排到啊a,b之前的,因为c的值对a,b都有依赖。但是,a,b的赋值语句可能会重排。这种指令重排在单线程环境中没有任何问题,但是在多线程的环境下,就将会出现数据的不确定性。

volatile保证可见性和有序性,但不能保证原子性

在Java中,大佬们给我们提供了volatile关键字来保证可见性和有序性。这个说法有两种语义:

最后要强调一点的是,volatile不能保证原子性。

public class Test {
    public volatile int inc = 0;
 
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

这段代码最终的输出结果将小于10000,原因就在于inc++它不是个原子性操作,尽管volatile能保证对inc的修改立即被其他线程感知,但是对于inc的并发读取不会触发强制刷新主内存,也不会导致其他线程的缓存行无效,这样就导致多个线程读取到同样的值。

一般这种情况可以用synchronizedlock来解决,也可以通过无锁CAS方式的AtomicInteger来解决。

所以说,重量级锁还是比较稳的,volatile不能完全替代synchronized,使用volatile必须具备两个条件:

参考资料

  1. 方腾飞:《Java并发编程的艺术》
  2. 指令重排序
  3. 深入分析volatile的实现原理
上一篇下一篇

猜你喜欢

热点阅读