从硬件缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析、单例模式案例

举个简单的例子,比如下面的这段代码:

i = i + 1;
  • 当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
  • 这个代码在单线程中运行是没有任何问题的,但是在 多线程中运行就会有问题了( 存在临界区)。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存区(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。

比如有两个线程像下列执行顺序:

​ 类似上面这种情况即为 缓存一致性问题读写场景、 双写场景都会存在缓存一致性问题,但 读读不会。前提是需要在多线程运行的环境下,并且需要多线程去访问同一个共享变量。

​ 这里的共享又可以回到上文中,即为上面所说,他们每个线程都有自己的高速缓存区,但是都是从同一个主存同步获取变量。

那么这种问题应该怎样解决呢?

问题线程为什么会不安全?

​ 答:共享资源不能及时同步更新,归根于 分时系统 上下文切换时 指令还未执行完毕 (没有写回结果) 更新异常

​ 众所周知现在的互联网大型项目,都是采用分布式架构同时具有其 “三高症状”高并发、高可用、高性能。高并发为其中最重要的特性之一,在高并发场景下并发编程就显得尤为重要,其并发编程的特性为 原子性、可见性、有序性

原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题。

  • 变量赋值问题:
  • b 变量赋值的底层字节码指令被分为两步:第一步先定义 int b;第二步再赋值为 10。
  • 两条指令之间不具有原子性,且在多线程下会发生线程安全性问题
int b = 10;

可见性指的是当前线程对共享变量的修改对其他线程来说是可见的。以下案例中假设不会出现多线程原子性问题(比如多个线程写入覆盖问题等),即保证一次变量操作底层执行指令为原子性的。

例如上述变量在读写场景下,不能保证其可见性,导致写线程完成修改指令时但为同步到主存中,读线程并不能获得最新值。这就是对于B线程来说没有满足可见性。

  • 案例解析: final关键字
  • final 变量可以保证其他线程获取的该变量的值是唯一的。变量指成员变量或者静态变量
  • b 变量赋值的底层字节码指令被分为两步:第一步先定义 int b;第二步再赋值为 10
final a = 10;             int b = 10;
  • final修饰的变量在其指令后自动加入了写屏障,可以保证其变量的可见性
  • a 可以保证其他线程获取的值唯一;b 不能保证其他线程获取到的值一定是 10,有可能为 0。
  • 读取 final 变量解析 :
    • 不加 final 读取变量时去堆内存寻找,final 变量是在栈空间,读取速度快
    • 读取 final 变量时,直接将其在栈中的值复制一份,不用去 getstatic ,性能得到提升
    • 注意:不是所有被 final 修饰的变量都在栈中。当数值超过变量类型的 MAX_VALUE 时,将其值存入常量池中
    • 读取变量的速度:栈 > 常量池 > 堆内存
  • final 可以加强线程安全,而且符合面向对象编程开闭原则中的close,例如子类不可继承、方法不可重写、初始化后不可改变、非法访问(如修饰参数时,该参数为只读模式)等

有序性指的是程序执行的顺序按照代码的先后顺序执行。

在Java中有序性问题会时常出现,由于我们的JVM在底层会对代码指令的执行顺序进行优化(提升执行速度且保证结果),这只能保证单线程下安全,不能保证多线程环境线程安全,会导致指令重排发生有序性问题。

案例: 排名世界第一的代码被玩坏了的单例模式

DCL(double checked):加入 volatile 保证线程安全,其实就是保证有序性。

上代码:其中包括了三个问题并且有详细注释解释。(鸣谢itheima满一航老师)

final class SingletonLazyVolatile {
    private SingletonLazyVolatile() { }
    // 问题1:为什么加入 volatile 关键字?
    // 答:   防止指令重排序 造成返回对象不完整。 如 TODO
    private static volatile SingletonLazyVolatile INSTANCE = null;
    // 问题2:对比实现3(给静态代码块加synchronized) 说出这样做的意义?
    // 答:没有锁进行判断、效率较高
    public static SingletonLazyVolatile getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        // 问题3:为什么要在这里加空判断,之前不是判断过了吗?
        // 答:假入t1 先进入判断空成立,先拿到锁, 然后到实例化对象这一步(未执行)
        //    同时 线程 t2 获取锁进入阻塞状态,若 t1 完成创建对象后,t2 没有在同步块这进行判空,t2 会再新创建一个对象,
        //    导致 t1 的对象被覆盖 造成线程不安全。
        synchronized (SingletonLazyVolatile.class) {  // t1
            if (INSTANCE != null) {
                return INSTANCE;
            }
            INSTANCE = new SingletonLazyVolatile();   // t1  这行代码会发生指令重排序,需要加入 volatile
            // 如:先赋值指令INSTANCE = new SingletonLazyVolatile,导致实例不为空,下一个线程会判空失败直接返回该对象
            // 但是构造方法()指令还没执行,返回的就是一个不完整的对象。
            return INSTANCE;
        }
    }
}

通过对并发编程的三要素介绍,也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

补充volatile知识:

  • volatile只保证可见性(多线程下对变量的修改是可见的)、有序性(禁止进行指令重排序)
  • volatile 的底层实现原理是 内存屏障(内存栅栏),Memory Barrier(Memory Fence),内存屏障会提供3个功能:
  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
  • 它会强制将对缓存的修改操作立即写入主存
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效
  • volatile修饰之后的变量会加入 读写屏障
  • 写屏障(sfence):保证在该屏障之前的,对共享变量的改动,都同步到主存当中
  • 读屏障(lfence):保证在该屏障之后的, 对共享变量的读取,加载的是主存中的最新数据
  • 对 volatile 变量的 写指令后会加入写屏障
  • 对 volatile 变量的 读指令前会加入读屏障

关于 volatile的用途像两阶段终止、单例双重锁等等:

两阶段终止–volatile

    @Log
    public class TwoPhaseStop {

        // 监控线程
        private Thread monitorThread;

        // 多线程共享变量 单线程写入(停止线程) 多线程读取 使用 volatile
        private volatile boolean stop = false;

        // 启动监控线程
        public void start() {
            monitorThread = new Thread(() -> {
                log.info("开始监控");
                while (true) {
                    log.info("监控中");
                    Thread currentThread = Thread.currentThread();
                    if (stop) {
                        log.info("正在停止");
                        break;
                    }
                    try {
                        log.info("正常运行");
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        // sleep出现被打断异常后、被打断后会清除打断标记
                        // 需要重新打断标记
                        currentThread.interrupt();
                    }
                }
                log.info("已停止");
            },"monitor");
            monitorThread.start();
        }

        // 停止监控线程
        public void stop() {
            stop = true;
            monitorThread.interrupt();
        }

    }

·
·
·
·

下篇预告:synchronized 和 volatile 区别和底层原理

Original: https://www.cnblogs.com/malongfeistudy/p/16750965.html
Author: lam要努力
Title: 从硬件缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析、单例模式案例

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/801518/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球