• 12 原子性|可见性|有序性|JMM内存模型


    目录

    1 并发三大特性

    1.1 原子性

    1.2 可见性

    1.3 有序性

    2 Java内存模型JMM

    2.1 JMM的抽象结构

    2.2 主内存与工作内存交互协议

    2.3 锁的内存语义

    2.4 volatile内存语义

    2.4.1 volatile写的语义

    2.4.2 volatile读的语义

    2.4.3 volatile内存语义实现原理


    1 并发三大特性

    1.1 原子性

            一个或多个操作,要么全部执行,要么全部不执行。Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,但不采取任何原子性保障措施的自增操作不是原子性的,如:i++

    1. public class AtomicTest {
    2. static int count = 0;
    3. public static void main(String[] args) throws InterruptedException {
    4. for (int i = 0; i < 10; i++) {
    5. new Thread(()->{
    6. for (int j = 0; j < 1000; j++) {
    7. count++;
    8. }
    9. }).start();
    10. }
    11. Thread.sleep(2000);
    12. System.out.println(count);
    13. }
    14. }

             上述结果每次执行都不一致,说明发生了线程安全问题

    如何保证原子性?

            sychronized、lock锁、CAS(AtomicInteger)

    1.2 可见性

            多个线程访问同一变量,一个线程对其进行修改,其它线程能及时感知

    如何保证可见性?

            volatile、内存屏障、sychronized、lock锁

    1.3 有序性

            程序执行的顺序按代码先后顺序执行(为了提升性能,编译器和处理器常常会对指令做重排序,这就可能引发有序性问题)

            Java有序性依赖内存屏障

    如何保证有序性?

            volatile、内存屏障、sychronized、lock锁

    2 Java内存模型JMM

            并发编程需解决的问题:

                    1 多线程间如何通信(线程间以何种机制交换数据)

                    2 多线程间如何同步(不同线程间操作发生的相对顺序)

    2.1 JMM的抽象结构

            JMM决定一个线程对共享变量的写入何时对另一个线程可见

            JMM定义了线程和主存之间的抽象关系

                    1 共享变量存在主存

                    2 每个线程又有自己私有的本地内存

                    3 本地内存存共享变量的副本

                    4 线程对共享变量的所有操作都必须在本地内存中进行,不能直接读取主存

             线程A和线程B要通信的话,必须经历以下两个步骤:

                    线程A把本地内存A更新过的共享变量更新到主内存

                    线程B到主内存读取线程A更新过的共享变量

            注:线程A无法直接访问线程B的工作内存,线程间通信必须经过主存

    2.2 主内存与工作内存交互协议

            八大原子操作:

                    lock:作用于主内存,把一个变量标识为线程独占状态(主内存)

                    unlock:释放主内存独占状态的变量(主内存)

                    read:主内存变量传输到工作线程中(主内存)

                    load:将read操作得到的变量放入工作内存的变量副本中(工作内存)

                    use:工作内存中的变量传递给执行引擎(工作内存)

                    assgin:从存储引擎得到的值,赋值给工作内存的变量(工作内存)

                    store:工作内存的一个变量值传到主内存(工作内存)

                    write:将store操作从工作内存的变量值放到主内存变量(主内存)

            八大原子操作必须满足的规则

                    1 把一个变量从主存复制到工作内存,必须顺序地按照read和load操作;把变量从工作内存同步回主存中,必须顺序地按store和write操作

                    2 不允许 read load store write操作单独出现

                    3 不允许一个线程丢弃assgin,变量在内存中改变之后必须同步到主存

                    4 不允许一个线程未发生assgin就从工作内存同步到主存

                    5 一个新的变量只能在主存中诞生,不允许在工作内存中使用一个未被初始化的变量

                    6 一个变量同一时刻只允许一个线程进行lock,但可以被同一线程重复执行多次,但也需执行相同次数的unlock操作

                    7 若对一个变量执行lock操作,会清空工作内存中此变量的值(执行引擎使用这个变量必须重新load或assign

                    8 不允许unlock一个未被lock的变量

                    9 对一个变量执行unlock之前,必须先把该变量同步到主内存中(执行store和write)

    可见性问题的产生:

            线程B对变量flag的修改,并不会让线程A感知,只有当线程B对主存共享变量flag进行lock时,线程A才会重新去主存中获取

    Java中可见性底层的两种实现方式:

            1 内存屏障(sychronized、Thread.sleep(10)、volatile)

            2 CPU上下文切换(Thread.yield()、Thread.sleep())

    2.3 锁的内存语义

            锁释放和释放锁的内存语义:

                    线程获取锁时:JMM把该线程对应的本地内存置为无效

                    线程获取锁时:JMM把该线程对应的本地内存中变量刷新到主存中

            sychronized关键字的作用是确保多个线程访问共享资源时的互斥性和可见性;在获取锁之前,线程会将共享变量的最新值从主存 -> 工作内存释放锁后会将修改的值刷到主存中,保证可见性

    2.4 volatile内存语义

    2.4.1 volatile写的语义

            当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主存中

    1. public static void main(String[] args) throws InterruptedException {
    2. for (int i = 0; i < 10; i++) {
    3. new Thread(() -> {
    4. for (int j = 0; j < 10000; j++) {
    5. count++; //static volatile int count = 0;
    6. }
    7. }).start();
    8. }
    9. Thread.sleep(10000);
    10. System.out.println(count);
    11. }

            上述结果会有所不同,原因:当变量被use到执行引擎中时,volatile并不能改变执行引擎中的值,当变量在执行引擎assign回工作内存中时,会发生覆盖

    2.4.2 volatile读的语义

            当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,需要从主存中读取共享变量(volatile读能保证每次读都是最新的数据)

    2.4.3 volatile内存语义实现原理

    禁止指令重排序:

            当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序

            当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序

            当第一个操作是volatile写,第二个操作是volatile读时,不能重排序

    示例:

    1. private static Singleton singleton;
    2. /**
    3. * 双重检查锁定(Double-checked Locking)实现单例对象的延迟初始化
    4. *
    5. * @return
    6. */
    7. public static Singleton getSingleton() {
    8. if (singleton == null) {
    9. synchronized (Singleton.class) {
    10. 正确的用法应该是使用volatile修饰singleton
    11. 原因就在于singleton = new Singleton()这行代码,创建了一个对象。这行代码可以分解为三行伪代码上面23之间可能会被重排序,重排序之后的执行时序如下:为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型
    12. 的处理器重排序。JMM内存屏障插入策略:
    13. 1. 在每个volatile写操作的前面插入一个StoreStore屏障
    14. 2. 在每个volatile写操作的后面插入一个StoreLoad屏障
    15. 3. 在每个volatile读操作的后面插入一个LoadLoad屏障
    16. 4. 在每个volatile读操作的后面插入一个LoadStore屏障
    17. 上述内存屏障的插入策略非常保守,但它可以保证在任意处理器平台,任意程序中都能得到正确的
    18. volatile内存语义。
    19. if (singleton == null) {
    20. singleton = new Singleton();
    21. }
    22. }
    23. }
    24. return singleton;
    25. }

     应该加上volatile修饰,因为new 对象分为三步,不能保证其原子性:

    private volatile static Singleton singleton;

    上述三步完全可能被重排序成:

  • 相关阅读:
    uniapp 微信小程ios端键盘弹起后导致页面无法滚动
    科研基础与工具(论文搜索)
    5.浮点数及其运算
    关于数据中心的设计方案,数据中心网络规划设计
    Linux环境下Lua安装
    Java毕业设计项目_企业级实战全栈项目中信CRM
    什么是代理IP池?真实测评IP代理商的IP池是否真实?
    聚观早报 | iPhone接口将与安卓统一;《三体》动画定档12月3日
    实时渲染方程
    Java随笔-TCP/IP网络数据传输
  • 原文地址:https://blog.csdn.net/m0_61253315/article/details/134052455