JUC - 多线程之JMM;volatile

Posted MinggeQingchun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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中,对基本数据类型的变量和赋值操作都是原子性操作 

i = 0;

2、可见性

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

3、有序性(指令重排)

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

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

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

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、线程传递性原则:A happen-before B, B happen-before C ,那么A 一定 happen-before C
 

二、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解决多个线程访问资源的同步性

以上是关于JUC - 多线程之JMM;volatile的主要内容,如果未能解决你的问题,请参考以下文章

JUC高级多线程_11:JMM与Volatile的具体介绍与使用

Juc13_JVM-JMM-CPU底层执行全过程缓存一致性协议MESI

JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码

Java并发之volatile关键字

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)