07、JUC源码分析:多线程之JMM;volatile

一、JMM

Java Memory Model(JMM)Java内存模型,区别与java内存结构。JMM定义了一套在多线程读写共享数据(变量、数组)时,对数据的可见性、有序性和原子性的规则和保障

(一)JMM结构规范

JMM规定了所有的变量都存储在主内存(Main Memory)中

每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)

不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成

在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响

(二)主内存和本地内存结构

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置

*

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证

*

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类 型的变量来说,load、store、read和write操作在某些平台上允许例外)

1、 lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;

2、 unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

3、 read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;

4、 load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;

5、 use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;

6、 assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;

7、 store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;

8、 write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

JMM对这八种指令的使用,制定了如下规则

1、 不允许read和load、store和write操作之一单独出现即使用了read必须load,使用了store必须write;

2、 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存;

3、 不允许一个线程将没有assign的数据从工作内存同步回主内存;

4、 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量就是怼变量实施use、store操作之前,必须经过assign和load操作;

5、 一个变量同一时间只有一个线程能对其进行lock多次lock后,必须执行相同次数的unlock才能解锁;

6、 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值;

7、 如果一个变量没有被lock,就不能对其进行unlock操作也不能unlock一个被其他线程锁住的变量;

8、 对一个变量进行unlock操作之前,必须把此变量同步回主内存;

(三)JMM三个特征

Java内存模型保证了并发编程中数据的原子性、可见性、有序性

1、原子性

原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰

多线程情况下,对同一个对象进行操作时,会导致字节码指令交错执行,从而产生原子性问题,可以通过synchronize关键字解决

原子性操作指相应的操作是单一不可分割的操作。在我们学化学这门课程的时候,对于里面讲到的原子性相信大家都非常明白,原子是微观世界中最小的不可再进行分割的单元,原子是最小的粒子。java里面的原子性操作也是如此,它代表着一个操作不能再进行分割是最小的执行单元。

原子性类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态

i = 0;       //1
j = i ;      //2
i++;         //3
i = j + 1;   //4

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是

1 在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 
2 中包含了两个操作:读取i,将i值赋值给j 
3 中包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 
4 中同三一样

在Java中,对基本数据类型的变量和赋值操作都是原子性操作

```java 
i*=*0;

#### 2、可见性 ####

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改

#### 3、有序性(指令重排) ####

如果在本线程内观察,所有的操作都是有序的;(线程内表现为串行的语义)如果在一个线程中观察另外一个线程,所有的操作都是无序的

多线程情况下,**JVM会进行指令重排**,会影响有序性

>有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可

```java 
int*i*=*0;************//语句1**
boolean*flag*=*false;*//语句2
i*=*1;****************//语句3**
flag*=*true;**********//语句4

上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行

JMM提供了内置解决方案**(happen-before 原则)及其外部可使用的同步手段(synchronized/volatile** 等),确保了程序执行在并发编程中的 原子性可视性及其有序性

(四)happen-before原则

happen-before是在JMM中用来实现并发编程中的有序性的。主要包括了以下八个规则:

1、 程序顺序性原则:应该线程按照代码的顺序执行;

2、 锁原则:如果一个对象已经加锁,那么后续的再对其加锁,一定发生在解锁之后;

3、 对象终结原则:对象的构造函数一定发生在对象终结之前;

4、 volatile变量规则:被volatile修改的变量写操作,Happens-Before于任意后续对这个变量操作的读;

跟线程相关的4个原则

1、 线程启动原则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作;

2、 线程中断原则:线程中断发生在线程中断检查之前;

3、 线程的结束原则:如果线程A中执行了ThreadB.join(),那么线程B的所有操作都发生在线程A的ThreadB.join()之后的操作;

4、 线程传递性原则:Ahappen-beforeB,Bhappen-beforeC,那么A一定happen-beforeC;

二、volatile

volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符,用来修饰变量

volatile关键字保证变量对所有线程****可见性,禁止指令重排,但是不保证原子性

(一)保证可见性

如下,我们有2条线程 t1 和 main主线程,num在主线程中改为1,但是分支线程t1 并不知道num已经变为1 ,还在根据 num == 0进行循环,程序一直在运行

*

此时,我们将全局变量 num 加上volatile修饰,t1线程立马结束循环

/**
 * volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
 * 1、volatile保证可见性
 */
public class VolatileTest {
    // 不加 volatile 程序就会死循环!
    // 加 volatile 可以保证可见性
    private volatile static int num = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            while (num == 0){ // 线程 t1 对主内存主线程 num = 1 的变化不知道

            }
        },"t1").start();

        try{
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 主线程中修改 num = 1
        num = 1;
        System.out.println(num);
    }
}

*

(二)不保证原子性

public class VolatileTest1 {
    private volatile static int num = 0;

    public static void main(String[] args) {
        // 开启10条线程,每条线程执行1000次循环+1,理论结果执行完成 num=10000
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ // Java中默认开启了2条线程 main  gc
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " num = " + num);
    }

    public static void add(){
        num++;
    }
}

不管执行多少次,会发现 num 都不是 10000

*

如果不使用 Lock 和 Synchronized ,解决 volatile不保证原子性问题;使用 java.util.concurrent.atomic 包下的原子类操作

*

此时妥妥的稳稳的输出 num = 10000

/**
 * volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
 * 2、volatile 不保证原子性
 *
 * 解决 volatile不保证原子性问题;使用java.util.concurrent.atomic 包下的原子类操作(不使用Lock和Synchronized)
 */
public class VolatileTest2Atomic {
    // volatile 不保证原子性
    //private volatile static int num = 0;
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void main(String[] args) {
        // 开启10条线程,每条线程执行1000次循环+1,理论结果执行完成 num=10000
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ // Java中默认开启了2条线程 main  gc
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " num = " + num);
    }

    public static void add(){
        // 不是一个原子性操作
        //num++;

        // AtomicInteger + 1 方法, CAS
        num.getAndIncrement();
    }
}

(三)禁止指令重排

有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可

int*i*=*0;************//语句1**
boolean*flag*=*false;*//语句2
i*=*1;****************//语句3**
flag*=*true;**********//语句4

上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行

/**
 * volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
 * 3、volatile 禁止指令重排
 */
public class VolatileTest3 {
    private static VolatileTest3 volatileTest3;
    private static boolean isInit = false;

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            volatileTest3 = null;
            isInit = false;

            new Thread(() -> {
                volatileTest3 = new VolatileTest3();    //语句1
                isInit = true;                          //语句2
            }).start();

            new Thread(() -> {
                if(isInit){
                    volatileTest3.doSomething();
                }
            }).start();
        }
    }

    public void doSomething() {
        System.out.println("doSomething");
    }
}

*

我们所期望的结果应该是每次都会打印doSOmething,可是这里会报空指针异常,出现这种情况的原因就是因为指令重排导致,上面语句1和语句2最终执行顺序可能会变为语句2先执行,语句1还未执行,此时刚有有一个线程独到了isInit的值为true,此时通过对象取调用方法就报空指针,因为此时SerialTest对象还未被实例化

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确

volatile关键字修饰时编译后会多出一个lock前缀指令

lock指令相当于一个内存屏障:重排序时不能把后面的指令重排序到内存屏障之前的位置

1、 在每个volatile写操作的前面插入一个StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

2、 在每个volatile写操作的后面插入一个StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序;

3、 在每个volatile读操作的后面插入一个LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序;

4、 在每个volatile读操作的后面插入一个LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序;

*

(四)volatile和synchronized区别

1、 volatile是线程同步的轻量级实现,性能比synchronize好;

2、 volatile只能修饰变量,而synchronize可以修饰方法、代码块和变量;

3、 volatile多线程时不会发生阻塞,而synchronize会阻塞线程;

4、 volatile可以保证可见性和有序性(禁止指令重排),无法保证原子性,而synchronize都可以保证;

volatile就是保证变量对其他线程的可见性和防止指令重排序
而synchronize解决多个线程访问资源的同步性