并发编程模型

  • 1.两个关键问题

  • 1)线程之间如何通信

  • 共享内存
    程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信

  • 消息传递
    程之间没有公共状态,线程之间必须通过发送消息来显式进行通信

  • 2)线程之间如何同步

  • 线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信

  • 2.抽象结构

  • 1)本地内存

  • 每个线程都有一个私有的本地内存(LocalMemory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化

  • 2)主内存

  • 线程之间的共享变量存储在主内存

附图:
【并发编程】- 内存模型(针对JSR-133内存模型)篇-LMLPHP


  • 3.重排序


  • 定义:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

  • 1) 3种类型

  • 编译器优化的重排序(处理器重排序)
  • 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  • 指令级并行的重排序(处理器重排序)

【并发编程】- 内存模型(针对JSR-133内存模型)篇-LMLPHP

  • 2)数据依赖性


  • 说明:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

  • 存在三种类型(只要重排两个操作执行顺序,结果便会被改变)

    • 写后读:写一个变量之后,再读这个变量
    • 写后写:写一个变量之后,在写这个变量
    • 读后写:读一个变量之后,再写这个变量
  • 3)as-if-serial语义

  • 说明:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
    • 顺序规则:A happens-before B,B happens-before C,happens-before C
      那么实际执行是B可以排在A前,JMM允许这种排序。
  • 4)对多线程的影响

  • 对于存证控制依赖的操作重排序,可能会改变程序的执行结果。
  • 5)DCL问题(double check lock)

    public class DoubleCheckedLocking { // 1
        private static Instance instance; // 2

        public static Instance getInstance() { // 3
            if (instance == null) { // 4:第一次检查
                synchronized (DoubleCheckedLocking.class) { // 5:加锁
                    if (instance == null) // 6:第二次检查
                        instance = new Instance(); // 7:问题的根源出在这里
                } // 8
            } // 9
            return instance; // 10
        } // 11
    }
  • 那么问题就出现在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还
    没有完成初始化
    • __根源:
    memory = allocate();  //1:分配对象的内存空间
    ctorInstance(memory);  //2:初始化对象
    instance = memory;   //3:设置instance指向刚分配的内存地址
  • JMM允许上述命令的执行顺序调整为
    memory = allocate();  //1:分配对象的内存空间
    instance = memory;   //3:设置instance指向刚分配的内存地址
                          //注意,此时对象还没有被初始化!
    ctorInstance(memory); //2:初始化对象
  • 问题:为什么要调整这个顺序呢?
  • 原因:这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。
    【并发编程】- 内存模型(针对JSR-133内存模型)篇-LMLPHP
    【并发编程】- 内存模型(针对JSR-133内存模型)篇-LMLPHP
  • 解决方案

    • 不允许2和3重排序([volatile]);
    • 允许2和3重排序,但不允许其他线程“看到”这个重排序。
  • 第一种基于volatile方案

    public class SafeDoubleCheckedLocking {
        private volatile static Instance instance;

        public static Instance getInstance() {
            if (instance == null) {
                synchronized (SafeDoubleCheckedLocking.class) {
                    if (instance == null)
                        instance = new Instance(); // instance为volatile,现在没问题了
                }
            }
            return instance;
        }
    }
  • 第二种基于类初始化方案
    public class InstanceFactory {
        private static class InstanceHolder {
            public static Instance instance = new Instance();
        }

        public static Instance getInstance() {
            return InstanceHolder.instance;  // 这里将导致InstanceHolder类被初始化,存在初始化锁,拿不到的线程会一直等待
        }
    }

  • 4.happens-before

1) 关系的定义

  • 如果⼀个操作happens-before另⼀个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。[JMM对程序员的承诺]
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)[JMM对编译器和处理器重排序的约束原则]

2) 规则

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • appens-before与JMM的关系
    【并发编程】- 内存模型(针对JSR-133内存模型)篇-LMLPHP

01-06 17:39