【JVM】一篇通关JMM内存模型

1. 原子性

  • 很多人将【java 内存结构】与【java 内存模型】傻傻分不清,【java 内存模型】是 Java Memory Model(JMM)的意思。
  • 简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性有序性、和原子性的规则和保障

1-1. 问题分析

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

  • 结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存线程内存中进行数据交换:

在这里插入图片描述

1-2. 问题解决

synchronized(同步关键字)

synchronized( 对象 ) {
    要作为原子操作代码
}

2. 可见性

2-1. 问题分析

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(run){
            // ....
        }
    });
    t.start();
    Thread.sleep(1000);
    run = false; // 线程t不会如预想的停下来
}

为什么会这样?

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

在这里插入图片描述
2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

在这里插入图片描述
3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

在这里插入图片描述

2-2. 问题解决

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存,保证了共享变量的可见性,但不能保证原子性

public class Demo1 {
    volatile static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
// ....
            }
        });
        t.start();
        Thread.sleep(1000);
        run = false; // 线程t不会如预想的停下来
    }

}

注意:
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但 缺点是synchronized是属于重量级操作,性能相对更低
如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也 能正确看到对 run 变量的修改了,想一想为什么?

进入println源码:

public void println(int x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

可以看出加了synchronized,保证了每次run变量都会从主存中获取

3. 有序性

3-1. 问题分析

看下面一个栗子:

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

看到这里可能聪明的小伙伴会想到有下面三种情况:

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但其实还有可能为0哦!😲

有可能还是:线程 2 执行 ready=true ,切换到线程1 ,进入if分支,相加为0,在切回线程 2 执行 num=2

这种现象就是指令重排

3-2. 问题解决

volatile 修饰的变量,可以禁用指令重排

int num = 0;
volatile boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

4. CAS与原子性

4-1. CAS

CAS 即 Compare and Swap ,它体现的一种乐观锁的思想

比如多个线程要对一个共享的整型变量执行 +1 操作:

// 需要不断尝试
while(true) {
    int 旧值 = 共享变量 ; // 比如拿到了当前值 0
    int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
    /*
	这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
	compareAndSwap 返回 false,重新尝试,直到:
	compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
	*/
    if( compareAndSwap ( 旧值, 结果 )) {
        // 成功,退出循环
    }
    //不一样,继续循环尝试
}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈多核 CPU 的场景下。

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

4-2. 乐观锁和悲观锁

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会

4-3. 原子操作类

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicIntegerAtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

5. synchronized 优化

  • 37
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值