JMM、Volatile(Juc-10)

本贴最后更新于 1620 天前,其中的信息可能已经东海扬尘

概述

谈谈你对 volatile 的理解

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是 JMM

java 内存模型,是一个不存在的东西,是一个概念,一个约定!

关于 JMM 一些同步的约定:

  1. 线程解锁前一定把共享变量立刻刷回主储存
  2. 线程加锁钱必须把主存最新值 copy 到工作内存中
  3. 加锁一定是同一把锁

线程,工作内存,主内存一共有 4 组 8 种操作

image.png

内存交互操作

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

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

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

    • 不允许 read 和 load、store 和 write 操作之一单独出现。即使用了 read 必须 load,使用了 store 必须 write
    • 不允许线程丢弃他最近的 assign 操作,即工作变量的数据改变了之后,必须告知主存
    • 不允许一个线程将没有 assign 的数据从工作内存同步回主内存
    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施 use、store 操作之前,必须经过 assign 和 load 操作
    • 一个变量同一时间只有一个线程能对其进行 lock。多次 lock 后,必须执行相同次数的 unlock 才能解锁
    • 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值
    • 如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量
    • 对一个变量进行 unlock 操作之前,必须把此变量同步回主内存

  JMM 对这八种操作规则和对 volatile 的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用 java 的 happen-before 规则来进行分析。

问题

程序不知道主内存的值已经被修改过了,类似这样的代码就会出现死循环

package net.yscxy.tvolatile; import java.util.concurrent.TimeUnit; /** * @Author WangFuKun * @create 2020/12/2 20:21 */ public class JmmTest { static int number = 0; public static void main(String[] args) throws InterruptedException {//main线程 new Thread(() -> { //线程1对主内存的变化是不可见的,不知道的 while (number == 0) { } }).start(); TimeUnit.SECONDS.sleep(1); number = 1; System.out.println(number); } }

volatile

  1. 保证可见性
package net.yscxy.tvolatile; import java.util.concurrent.TimeUnit; /** * @Author WangFuKun * @create 2020/12/2 20:21 */ public class JmmTest { static volatile int number = 0; public static void main(String[] args) throws InterruptedException {//main线程 new Thread(() -> { //线程1对主内存的变化是不可见的,不知道的 while (number == 0) { } }).start(); TimeUnit.SECONDS.sleep(1); number = 1; System.out.println(number); } }
  1. 不保证原子性

原子性:不可分割

也就是线程 A 在执行任务的时候是不能被打扰和被分割的,要么同时成功,要么同时失败

package net.yscxy.tvolatile; /** * @Author WangFuKun * @create 2020/12/2 21:39 */ //测试不保证原子性 public class VDemo02 { private volatile static int number = 0; public static void add() { number++; } public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { add(); } }).start(); } //因为默认有jc线程和主线程这两个线程一直活跃着 while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(number); } }

如果不加 lock 或者 synchronized 如何保证原子性

image.png

方案:使用原子类,解决原子性问题 AtomicInteger

AtomicInteger 他的底层用的是 CAS

package net.yscxy.tvolatile; import java.util.concurrent.atomic.AtomicInteger; /** * @Author WangFuKun * @create 2020/12/2 21:39 */ //测试不保证原子性 public class VDemo02 { //使用原子类进行解决 private volatile static AtomicInteger number = new AtomicInteger(); public synchronized static void add() { //number++ //当然这里并不简单的是一个加一操作 number.getAndIncrement(); //AtomicInteger +1方法,CAS } public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { add(); } }).start(); } //因为默认有jc线程和主线程这两个线程一直活跃着 while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(number); } }

这些类的低层直接和操作系统挂钩,在内存中修改值,Unsafe 是一个很特殊的存在

  • JUC
    17 引用 • 3 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...