第十二章 Java内存模型与线程

Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值的底层细节。

12.1 Java内存模型

  12.1.1 主内存和工作内存

  所有变量都存储在主内存中,每个线程有自己的工作内存,不同的线程不能互相访问工作内存,所有数据必须从主内存中获取。

  12.1.2 内存间交互操作

  Java内存模型,定义了8种操作,每个操作必须保证是原子的。

  1. lock:作用于主内存,将一个变量标识为某线程独有的状态。
  2. unlock:作用于主内存,将一个变量标识解除锁定。必须先有锁定操作才能解锁。
  3. read:作用于主内存,将一个变量的值传输到工作内存中。
  4. load:作用于工作内存,将主内存同步过来的变量放入工作内存的变量副本中。
  5. use:作用于工作内存,将一个变量的值传递给执行引擎。
  6. assign:作用于工作内存,将从执行引擎中得到的值赋值给工作内存中的变量
  7. store:作用于工作内存,将工作内存的值传递个主内存。
  8. write:作用于主内存,将工作内存同步过来的值写入主内存变量。

  此外,还对上述操作有如下限制:

  1. 必须顺序执行read、load,store、write操作,但不要求连续执行
  2. 不允许read、load,store、write单独出现。即从主内存读取的值必须被工作内存接受,工作内存同步回主内存的值,主内存必须接受。
  3. 不允许一个线程丢弃最近的assign操作,必须同步回主内存。且只有通过assign操作后才能同步回主内存。
  4. 新的变量只能存主内存中产生,对一个变量使用use、store操作前必须先执行assign和load操作。
  5. 一个变量同一时间只能被一个线程lock,可以被同一线程lock多次,必须经过相同次数的unlock才能解锁。
  6. 如果对主内存的一个变量执行lock操作,会清空各个工作内存中此变量的值,在执行引擎使用该值时,需要重新load或assign操作。
  7. 如果一个变量没有被lock,则不允许执行unlock。也不允许unlock其他线程锁定的变量。
  8. 对一个变量unlock之前,必须将该变量同步回主内存。

  12.1.3 volatile型变量的特殊规则

  volatile型变量两个特性:(1)可见性 (2)禁止指令重排序

  当volatile变量被一线程修改时,该修改其余线程均可见。因为每次使用volatile变量时,都需要从主内存中刷新最新值。在不符合以下两条规则的场景中,需要通过加锁保证原子性。

  • 运算结果不依赖volatile变量的当前值,或者确保只有单一的线程改动变量的值。
  • 变量不需要与其他的状态变量共同参与不变性约束。

  关于禁止指令重排序,有一个双重检测的例子

 1     private volatile static Object reference;
 2 
 3 
 4     public Object getObject() {
 5         if (reference == null) {
 6             synchronized (Object.class) {
 7                 if (reference == null) {
 8                     reference = new Object();
 9                 }
10             }
11         }
12         return object;
13     }

 

  代码第6行加锁,确保同一时刻只有一个线程进行对象的初始化操作。可能会出现的问题是,另一个线程拿到了未初始化完成的对象。

  忽略类加载过程,创建一个新的对象步骤共三步:1. 分配内存 2.执行实例构造方法初始化对象 3.将对象引用指向分配的内存。

  对于非volatile变量,存在指令重排的问题,譬如重排后执行顺序为1、3、2。线程A执行完步骤3将引用指向了分配好的内存,还未执行步骤2时,线程B此时执行到代码第五行,对象引用已经不为空,获取了未完成初始化的实例。

  而volatile变量,会在在引用赋值指令后额外插入一条指令:lock add1 $0x0,(%esp)。add1 $0x0,(%esp)(将ESP寄存器的值加0)是一个空操作,关键在于lock前缀,它的作用是将本处理器的缓存写入内存,并使其他处理器或内核的无效化其缓存。结合java内存模型来讲,即当前线程工作内存的变量同步至主内存,并且,使得其余线程的工作内存中的变量失效。这一点保证了volatile变量的可见性。至于怎么禁止指令重排,lock操作将修改同步至内存时,确保之前的操作均已完成。其余线程会获取最新的reference引用,并且是完全初始完成的引用。

  Java内存模型对于volatile变量额外做出了如下限制:

  1. 使用volatile变量必须从主内存获取最新值
  2. 修改完volatile变量必须立即同步回主内存
  3. 若线程对volatile变量A的use动作先于对volatile变量B的use动作,那么对A的read操作也必定先于对B的read操作。类似的对于assign也有一样的规则。

  12.1.4 原子性、可见性和有序性

  1. 原子性:8种内存操作动作均是原子的,其中lock和unlock对应字节码指令monitorenter和monitorexit,反应到Java代码中就是synchronized关键字。

  2. 可见性:一个线程修改了变量的值,其余线程立即得知修改后的结果。除了volatile变量之外,synchronized和final也能保证可见性。同步块的可见性在于unlock之前,必须将变量同步到主内存。而final的可见性是指,一旦在构造器中初始化完成,其他线程就能看到final字段的值。

  3. 有序性:观察线程本身内部操作都是有序的,观察另一个线程则操作都是无序的。无序主要是指指令重排和工作内存与主内存同步延迟。除了volatile变量之外,synchronized关键字也能保证有序性,因为同一时刻只有一个线程能对变量A执行lock操作,持有同一个锁的两个同步块只能串行进入。

12.2 Java与线程

  12.2.1 状态转换

  线程的6种状态:

  • 新建:创建后尚未启动
  • 运行:包含running和ready,即正在执行和等待操作系统分配时间
  • 无限等待:不会被操作系统分配时间,需要其他线程显示唤醒。以下方法会另线程进入无限等待
    • 没有设置超时时间的Object::wait()方法
    • 没有设置超时时间的Thread::join()方法
    • LockSupport::park()方法
  • 限时等待:不会被操作系统分配时间,在一定时间后会自动唤醒。
    • Thread::sleep()方法
    • 设置了超时时间的Object::wait()方法
    • 设置了超时时间的Thread::join()方法
    • LockSupport::parkNanos()方法
    • LockSupport::parkUntil()方法
  • 阻塞:线程因获取排他锁而等待
  • 结束:线程结束执行

  关于线程状态,可以参考:https://www.cnblogs.com/waterystone/p/4920007.html    

 

posted @ 2021-02-19 19:56  walker993  阅读(77)  评论(0编辑  收藏  举报