JAVA并发编程(七)-JMM(JAVA内存模型)


👶一、JMM(Java内存模型)

  JMM即Java Memory Model,它定义了内存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
-原子性-保证指令不会受到线程上下文切换的影响。(synchornized,前面的文章有介绍到)
-可见性-保证指令不会受到CPU缓存的影响。
-有序性-保证指令不会受到CPU指令并行优化的影响。

👧二、可见性

1、退不出的循环

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

public class CanNotBreakOutCircle {

    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() -> {
            while (run) {
            }
        });
        t.start();

        TimeUnit.SECONDS.sleep(1);

        run = false;
    }
}

执行结果:
在这里插入图片描述

接下来我们来分析一下:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
解决方案1:
加上volatile关键字

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

volatile static boolean run = true;

解决方案2:
加上synchronized

public class CanNotBreakOutCircle {

    static boolean run = true;

    final static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

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

                synchronized (lock) {
                    if (!run) {
                        break;
                    }
                }
            }
        });
        t.start();

        TimeUnit.SECONDS.sleep(1);

        synchronized (lock) {
            run = false;
        }
    }
}

2、可见性和原子性

在这里插入图片描述
在这里插入图片描述
synchronized语句块既可以保证代码的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。

🧒三、有序性

在这里插入图片描述
  现在这样看来似乎是没有什么影响,但这仅局限在单线程下是没有影响的,当多线程的情况下,却是有影响的。

1、鱼罐头的故事

在这里插入图片描述
在这里插入图片描述

2、指令重排序优化

在这里插入图片描述
在这里插入图片描述
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来 实现指令集并行,这一技术在80年代中叶到90年代中叶占据了计算架构的重要地位。
在这里插入图片描述

👦四、volatile原理

volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对volatile变量的写指令后会加入写屏障
对volatile变量的读指令前会加入读屏障

1、volatile保证可见性

在这里插入图片描述
在这里插入图片描述

2、volatile保证有序性

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

👩五、double-checked locking问题(dcl)

1、懒汉式实例化

在这里插入图片描述

  这段代码在多线程模式下是没有问题的,只要每一次我们都去获取锁,然后判断这个对象是否被创建了,如果被创建了我们就直接返回对象,如果还没有被创建,我们就创建对象。因为这段获取对象的代码都在synchronized下进行保护,所以这里是没有线程安全的问题的。但是这样子对于性能会比较不友好,每一次我们都去争抢锁,其实仔细观察这里的代码,你会发现我们只有第一次创建对象的时候需要加锁,而后面我们去获取对象的时候根本就不用加锁,所以我们还可以继续进行改进。

  于是就带来了double-checked locking
在这里插入图片描述

  这里当t1线程检测到对象为null的时候,会占用锁然后进行新建对象,与此同时t2线程也会检测到对象为null,并且试图去获取锁,而这里t2线程发现锁已经被t1所拥有了,于是这里t2线程会继续等待锁的释放。当t1线程创建了对象释放了锁,t2线程获取到锁之后会再次进行判断对象是否已经被创建了,如果被创建了,就直接返回对象,这就是双重检查的关键所在。这样子就实现了首次访问会加锁,而之后的使用就不需要加锁了。但是这样看上去似乎无懈可击的代码依然存在着问题

分析原因:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  当这里进行了指令重排之后,先调用了24行,给INSTANCE附上了值,但是还没有调用构造方法,意味着t2线程获取到的INSTANCE确实不为null,但是这时候t2线程去使用对象的时候,这个对象的构造方法还没有完成。
在这里插入图片描述
synchronized能保证原子性、可见性,但要根据实际情况来看是否能够保证有序性,当变量完全受synchronized保护的时候,对于指令的重排序,是不会受影响的。但如果变量存在不受synchronized保护的情况,那么当指令发生重排序的时候,可能会存在问题。

2、duoble-checked locking解决

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
加上了写屏障以后,写屏障之前的代码不能够重排序到写屏障之后了,这里的21行必然会在24行之前。 所以这里t2线程不会拿到不为null但是未执行构造方法的对象。

🧑六、happens-before规则

happens-before规定了对共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结。

以下的这些例子共享变量之间都是可见的
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值