JMM --- 内存模型

JMM是什么

JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

主存&工作内存&高速缓存

高速缓存 :当一个线程循环多次从主存中拿到值,这个时候jvm就会将这个值放到高速缓存中,你再想取这个值,就会拿到高速缓存里的

  • 例子 :退不出的循环,理论上来说,当主线程执行到最后面的时候,线程t1就会暂停,可是线程t1却暂停不了
public static boolean run = true;

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            while(run) {

            }
        }, "t1");

        t1.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("t1 Stop");
        run = false;
    }

  • 分析如下图:

在这里插入图片描述

  • 如何解决

    使用 volatile (易变关键字)

    它可以用来修饰成员变量和静态成员变量(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

public static volatile boolean run = true; // 保证内存的可见性

​ 例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile 变量的修改对另一个线程可见, 不能保证原子性仅用在一个写线程,多个读线程的情况。

  • 注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。

  • 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,想一想为什么?

  • 因为 printIn() 方法使用了 synchronized 同步代码块,可以保证原子性与可见性,它是 PrintStream 类的方法。

volatile原理

volatile的作用

  • 防止线程从高速缓存中获取数据,用volatile修饰的变量都要到主存里面去获取数据
  • 防止jvm的指令重排的问题

第一个作用就是上面的高速缓存的例子

我们来掩饰一下指令重排,那么什么是指令重排呢?

static int i = 0;
static int j = 0;

void main(){
		int i = ...; // 耗时很长
		int j = ...; // 耗时很短
}

在我们的理解里面,肯定是先执行的 i赋值,再执行 j赋值,然而在jvm的反编译里面,我们看到的是先进行了 j赋值(耗时较短的)再进行了 i赋值(耗时较长的),jvm认为只要保证最后的一致性就可以了,然而有些场景是会出现问题的,就拿我们的双检加锁的单例模式来说吧

public final class Singleton {
        private Singleton() { }
        private static volatile Singleton INSTANCE = null; // 加入了 volatile关键字
        public static Singleton getInstance() {
            // 实例没创建,才会进入内部的 synchronized代码块
            if (INSTANCE == null) {
                synchronized (Singleton.class) { // t2
                    // 也许有其它线程已经创建实例,所以再判断一次
                    if (INSTANCE == null) { // t1
                        INSTANCE = new Singleton();
                    }
                }
            }
            return INSTANCE;
        }
    }

这个volatile有什么作用呢?其实就是防止指令重排,在一个对象创建的过程中,它可能先new出来了,但是地址还没分配,此时synchronized外的线程进来就发现了这个对象new出来了,然后就拿到了这个对象,实际上这个对象还没被分配地址,然后一用,发现用的是null,此时我们添加了volatile防止对象创建时候的指令重排现象,就解决了单例模式会出现的问题。

CAS原理

CASCompare 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,所以线程不会陷入阻塞,这是效率提升的因素之一

  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

synchronized的结构

在这里插入图片描述

一个大的概念monitor

  • Owner :当前持有锁的线程

  • EntryList :竞争锁失败,且自旋失败的线程的阻塞队列

  • 剩下的不考虑了

synchronized优化

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的Hash码、分代年龄,当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容

1.轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

这就好比:

学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没 有竞争,继续上他的课。
如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程 A 随即升级为重量级锁, 进入重量级锁的流程。

而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来 假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object();
public static void method1() {
		synchronized( obj ) { // 同步块 A
				method2(); 
		}
}
public static void method2() {
		synchronized( obj ) { // 同步块 B
		} 
}

执行流程 :
在这里插入图片描述

在这里插入图片描述

2.重量级锁

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻 量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object();
public static void method1() {
		synchronized( obj ) { // 同步块
		}
}

上锁流程 :
在这里插入图片描述

在这里插入图片描述

3.自旋锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),

这时当前线程就可以避免阻塞。(阻塞是耗时的,还需要记录类信息等等…,所以能不阻塞最好,就出现了我们的自旋锁)

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能 性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 自旋成功的情况
    在这里插入图片描述

  • 自旋失败的情况
    在这里插入图片描述

4.偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁 来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS.

撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
访问对象的 hashCode 也会撤销偏向锁 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2, 重偏向会重置对象的 Thread ID

撤销偏向和重偏向都是批量进行的,以类为单位 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值