Thread - Volatile

引出volatile

volatile 挥发性的,不稳定的

第一点很好理解,第二点是什么意思呢,下面解释。

在解释之前,需要普及很多知识点。

参考了 -> 细说 Volatile https://gitbook.cn/books/5db54b448190041c0d50b530/index.html#volatile

指令重排序

首先要讲一个点,我们写的代码,编译成字节码指令,最终到CPU中的执行顺序,可能不是原来的顺序, CPU可以根据需要重排序,从而提高效率。也有人把这个称为乱序执行

比如如下代码:

a = a + 1;  // 语句1
b = b + 1;  // 语句2
a = a * 2;  // 语句3

我把语句2和语句3的顺序换下,这样,CPU可以一次性把a的值处理完,效率提高了。

当然,不是所有的指令都能被重排序,是有一个规则的。

参考 -> 当我们在谈论CPU指令乱序的时候,究竟在谈论什么?https://zhuanlan.zhihu.com/p/45808885

并发编程的三个概念

原子性

原子性:一个操作(或者多个操作),要么全部执行(并且执行的过程不会被任何因素打断),要么就都不执行。

比如a = b + c;,这一句话,在翻译成字节码(或者更底层的语言)后,会变成多个指令,如下:

步骤1:读取 b 的值
步骤2:读取 c 的值
步骤3:将 b 与 c 的值相加,赋值给 a

在多线程的情况下,有可能发生如下情形,线程1的操作的原子性就被打破了。

(线程1)步骤1:读取 b 的值
---------------------------
(线程2)给 c 重新赋值
---------------------------
(线程1)步骤2:读取 c 的值
(线程1)步骤3:将 b 与 c 的值相加,赋值给 a

可见性

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

int i = 0;

//线程1
i = 10;
 
//线程2
i++;

如果没有可见性,线程2执行完,值就是1。 如果有可见性,线程2执行完,值就是11。

有序性

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

我们知道CPU会对指令进行重排序,这在单线程情况下没有什么问题,但是多线程情况下可能就有问题了。

如下所示,线程1会先loadContext(),然后将flag置为true,线程2会一直等flag,直到变化了,才开始startProcess(context)。 如果语句1和语句2顺序颠倒,对于线程2来说,可能会出错,即没有初始化完成就开始处理。

//线程1
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited){
  sleep();
}
startProcess(context);

所以,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

参考 -> Java并发编程:volatile关键字解析 https://www.cnblogs.com/dolphin0520/p/3920373.html

后续思考

其实,原子性和有序性的破坏,本质是我们写代码用的是高级语言,而实际执行的是机器语言,而这两者有时候会存在一些差异。这个(可能)只存在于多线程(并发编程)。 而可见性,既存在于多线程(并发编程),也存在于多缓存(并行编程)。

Java内存模型

为了应对并发编程的三个问题,JMM - Java Memory Model - Java内存模型 做出了一些规定。

保证 - 原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

举例如下:只有语句1是原子性的,其它都不是。

x = 10;        //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

如果要实现更大范围操作的原子性,可以通过synchronizedLock来实现。

保证 - 可见性

Java提供了volatile关键字来保证可见性。

另外,通过synchronizedLock也能够保证可见性,synchronizedLock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

保证 - 有序性

Java通过volatile关键字来保证一定的”有序性”。

另外,通过synchronizedLock也能够保证有序性,使用锁把多线程在短时间内”变成”单线程,可以规避掉大多数并发问题。

另外,Java创造了 happen-before 规则,来保证有序性。

说了一大堆,记住一句话:当一个操作 A HB 操作 B,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

参考 -> Java 使用 happen-before 规则实现共享变量的同步操作

回到 volatile

volatile具体怎么操作呢?加内存屏障啊!

JMM规定了四种屏障:

那LoadLoad为例:

读操作1
LoadLoadBarrier
读操作2

保证读操作1完了之后,才能进行读操作2,顺序不能重排。

具体的实现是:在编译成字节码时,涉及变量a的读/写操作周围会加一层内存屏障。(但其实这里采用的是Lock锁来实现的,为啥?更方便,虽然性能差一点。)

volatile的原子性问题

需要注意的是,volatile无法保证原子性。 看下例:创建了10个线程,每个线程对一个共享 volatile 变量执行自增1万次。理论上,最后结果输出应该是10万次。

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<10000;j++) {
                        test.increase();
                    }
                    System.out.println(Thread.currentThread().getName() + ": " + test.inc);
                };
            }.start();
        }

        while(Thread.activeCount()>2) {
            //保证前面的线程都执行完
            //System.out.println(Thread.activeCount());
            Thread.yield();
        }

        System.out.println(test.inc);
    }
}

实际结果(值是小于10万次的随机数) ->

Thread-2: 19420
Thread-4: 30130
Thread-0: 38377
Thread-1: 47181
Thread-5: 57181
Thread-7: 67600
Thread-8: 76001
Thread-9: 86001
Thread-6: 96001
Thread-3: 96001
96001

原因:inc++这个操作不是原子性的。可以分为三步:

步骤1:读取inc的值
步骤2:将inc的值+1
步骤3:将新的值赋予inc

有可能,线程1执行完1,2步,然后就被线程2打断。线程2执行完了1,2,3步,然后再切换回线程1。线程1再执行步骤3,将线程2的结果覆盖了(总体里看,相当于少自增一次)。

怎么用volatile

由于volatile不保证原子性,使用起来要很小心。

总结来说,需要保证:

volatile的常用场景

  1. 状态量标记,多线程中需要共享一个flag
volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}
  1. 双重检查锁 double checked locking
public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

几个注意点:

参考 -> Java中的双重检查锁(double checked locking) https://www.cnblogs.com/xz816111/p/8470048.html

Fork me on GitHub