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

Posted ABin-阿斌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC高级多线程_11:JMM与Volatile的具体介绍与使用相关的知识,希望对你有一定的参考价值。

我是 ABin-阿斌:写一生代码,创一世佳话。 如果小伙伴们觉得我的文章有点 feel ,那就点个赞再走哦。
在这里插入图片描述

一、JMM 简介

1. 什么是 JMM

  • JMM 是一种 抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。
  • JMM 关于同步的规定:
    • 线程解锁前,必须把共享变量的值刷新回主内存

    • 线程加锁前,必须读取主内存的最新值到自己的工作内存

    • 加锁解锁是同一把锁

2. JMM的内存交互操作

  • 工作内存与主内存交互展示:

在这里插入图片描述

3. Java内存模型的八大操作

  • lock(锁定) : 作用于主内存的变量,把一个变量标识为一条线程独占的状态。
  • unlock(解锁) : 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取) : 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入) : 作用于工作内存的变量,把read操作所得到的值放入工作内存的变量副本中。
  • use(使用) : 作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值) : 作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储) : 作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入) : 作用于主内存的变量,把store操作所从工作内存中得到的变量的值放入主内存的变量中。

4. 执行八大操作要遵循的规则:

  • 不允许 read 和 loadstore 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况。
  • 不允许一个线程丢弃它的最近 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中——>诞生,不允许在工作内存中直接使用一个未被初始化( load 或 assign )的变量,就是对一个变量执行 use 和 store 之前必须先执行过了 assign 和 load 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,僵尸清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load或assign 操作初始化变量的值。
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store 和 write 操作)。

交互时会出现的问题: 如果线程B修改了值,但是线程A不能及时可见。

在这里插入图片描述

那么如何解决这个【可见性】问题呢?

  • 答: 使用 Volatile 关键字

二、Volatile 简介

  • volatileJava 提供的一种轻量级的同步机制,Java语言包含两种内在的同步机制: 同步块(或方法)和 volatile 变量,相比于 synchronized(synchronized 通常称为重量级锁),volatile 更轻量级。因为它不会引起线程上下文的切换和调度。但是 volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

1. 并发编程的3个基本概念

1.1 原子性:

  • 定义: 一个操作或者多个操作,要么全部成功要么全部失败,并且执行的过程不会被任何因素打断。

  • 注意:

    • 原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。
    • 简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。
    • 例如 A=1 是原子性操作,但是 A++ 和 A +=1 就不是原子性操作。
  • Java 中的原子性操作包括:

    • 基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
    • 所有引用 reference 的赋值操作
    • java.concurrent.Atomic.* 包中所有类的一切操作

1.2 可见性:

  • 定义: 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 注意:
    • 在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。
    • Java 提供了 volatile 来保证可见性,当一个变量被 volatile 修饰后,表示着线程本地内存无效。
    • 当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
    • 当然,synchronize 和 Lock都可以保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码。并且,在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。

1.3 有序性:

  • 定义: 表示程序执行的顺序按照代码的先后顺序执行

  • Java内存模型中的有序性可以总结为:

    • 如果在本线程内观察,所有操作都是有序的。
    • 如果在一个线程中观察另一个线程,所有操作都是无序的。
    • 前半句是指:线程内表现为串行语义,后半句是指:指令重排序现象和工作内存主主内存同步延迟现象。
  • 注意:

    • 在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。
    • Java 提供 volatile 来保证一定的有序性。最著名的例子就是单例模式里面的 DCL(双重检查锁)。
    • 另外,可以通过 synchronized 和 Loc k来保证有序性,synchronized 和 Loc k保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

2. volatile变量的特性

2.1 只保证了可见性,不保证原子性

  • 当写一个 volatile 变量时,JMM 会把该线程本地内存中的变量强制刷新到主内存中去。
  • 这个写会操作会导致其他线程中的 volatile 变量缓存无效。

2.2 禁止指令重排

内存屏障与CPU指令的两大作用:

  • 保证特定的操作的执行顺序
  • 可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)

volatile 禁止指令重排序也有一些规则:

  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
  • 执行到 volatile 变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见。

举例说明:

在这里插入图片描述
注意: Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

3.Volatile保证可见性案例展示:

  • 不加 volatile: 由于线程A 不知道主内存的值发生了改变,所以它会一直在循环,从而导致当前线程无法结束。
/**
 * @author ABin-阿斌
 * @description: 使用 Volatile 保证 JMM 在内存交互时出现的【不及时可见性问题】
 */
public class JMMDemo {
    //不加 volatile 程序就会死循环
    //volatile:保证可见性
    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        //目前线程A对主内存的变化并不知情
        new Thread(() -> {
            while (num == 0) {

            }
        }).start();

        TimeUnit.SECONDS.sleep(2);
        num = 1;
        System.out.println(num);

    }
}

4.Volatile不保证原子性案例展示:

  • 注意: add 方法中使用 synchronized修饰,输出的结果必然是:21000。使用 volatile 修饰,结果只有概率性出现:21000
/**
 * @author ABin-阿斌
 * @description: 验证 Volatile 不保证原子性
 */
public class VolatileDemo {
    //注意:volatile 不保证原子性
    private volatile static int num = 0;

    //注意:该方法并不是一个原子性操作
    public static void add() {
        num++;
    }

    public static void main(String[] args) {
        //预期结果:21000
        for (int i = 0; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        //activeCount():返回活动线程的当前线程的线程组中的数量
        //为什么是>2:因为我们java默认是两条线程,一条:main;一条:gc
        while (Thread.activeCount() > 2) {
            //线程让步
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}
  • 解答上述代码中 add() 方法为什么不是原子性,我们通过编译class文件去观察

在这里插入图片描述
纠正:cmd
在这里插入图片描述

在这里插入图片描述

5. 如果不加 lock 和 synchronized,怎么保证可见性

  • 答: 使用原子类 java.concurrent.Atomic.*,解决原子性问题

案例展示:

/**
 * @author ABin-阿斌
 * @description: 验证不加 lock 和 synchronized,怎么保证可见性
 */
public class VolatileDemo2 {
    //注意:volatile 不保证原子性
    private volatile static AtomicInteger num = new AtomicInteger();


    public static void add() {
        // 使用 AtomicInteger + 1 方法, CAS 保证原子性
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        //预期结果:21000
        for (int i = 0; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        //activeCount():返回活动线程的当前线程的线程组中的数量
        //为什么是>2:因为我们java默认是两条线程,一条:main;一条:gc
        while (Thread.activeCount() > 2) {
            //线程让步
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}

源码解析: 上述代码中的 num.getAndIncrement(); 方法为什么能保证原子性

在这里插入图片描述
通过: Unsafe,底层直接和操作系统挂钩,在内存中修改值。

在这里插入图片描述

5.1 Unsafe类的简单介绍

优点:

  • Unsafe 类是在 sun.misc 包下,不属于Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Cassandra、Hadoop、Kafka 等。
  • Unsafe 类在提升 Java 运行效率,增强Java语言底层操作能力方面起了很大的作用。

缺点:

  • Unsafe 类使Java拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。
  • 过度的使用 Unsafe 类会使得出错的几率变大,因此 Java 官方并不建议使用的,官方文档也几乎没有。

三、 指令重排

1. 什么是指令重排

  • 在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。
  • 但是,一般情况下,CPU 和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题。
  • 主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

2. 数据依赖性

  • 主要指不同的程序指令之间的顺序是不允许进行交互的,即可称这些程序指令之间存在数据依赖性。

举例说明:
在这里插入图片描述
在这里插入图片描述

以上是关于JUC高级多线程_11:JMM与Volatile的具体介绍与使用的主要内容,如果未能解决你的问题,请参考以下文章

JUC - 多线程之JMM;volatile

Juc11_Java内存模型之JMM八大原子操作三大特性读写过程happens-before

Juc11_Java内存模型之JMM八大原子操作三大特性读写过程happens-before

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

一篇神文就把java多线程,锁,JMM,JUC和高并发设计模式讲明白了

JUC高级多线程_08:线程池的具体介绍与使用