并发编程-锁相关的内存语义

锁的内存语义本质上可以说是对共享变量的更新,能及时让其他线程观察到;并且通过内存屏障,组织编译器或处理器指令重排序,导致多线程下不一致的现象。

1. volatile内存语义

  见上一篇文章。

2. 锁的内存语义

(1)锁的释放和获取的内存语义

  当线程释放锁时,JMM会将本地内存中的共享变量同步到主内存中;

  当线程获取锁时,JMM会将该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

(2)锁内存语义的实现

  ReentrantLock的实现依赖于AbstractQueuedSynchronizer(AQS),AQS使用一个整形的volatile变量(state)来维护同步状态。这个volatile变量是ReentrantLock实现的关键。

  编译器不会对volatile读与其后的任意内存操作重排序不会对volatile写与其前的任意操作重排序。CAS同时具有volatile读和写的内存语义,编译器不会对CAS前和后的任意内存操作重排序,其是通过底层处理器缓存锁定实现原子性的。

CAS同时具有volatile读和写的内存语义,故Java线程之间的通信存在4种方式:

  ① A线程写volatile变量,随后B线程读该变量;

  ② A线程写volatile变量,随后B线程使用CAS更新该变量;

  ③ A线程使用CAS更新volatile变量,随后B线程使用CAS更新该变量;

  ④ A线程使用CAS更新volatile变量,随后B线程读该变量。

concurrent包的实现,通用的实现模式:

  ① 声明共享变量为volatile

  ② 使用CAS的原子条件更新来实现线程之间的同步

  ③ 配合以volatile的读/写,及CAS所具有的volatile读和写的内存语义来实现线程间的通信。 

AQS、非阻塞数据结构和原子变量类,都是使用该模式来实现的。(该图摘自java concurrent包的实现原理

3. final内存语义

(1)final重排序规则

  ① 在类实例化时,构造函数中对一个final域的写入,与后边对该类对象的引用之间不能重排序。防止引用到未初始化完全的对象

  首先对象是共享对象如通过static修饰的类实例。JMM禁止编译器将final域的写重排序到构造函数之外;编译器会在final域写入之后,构造函数返回之前插入StoreStore屏障。

  对于普通变量的赋值可能重排序到构造函数之外,此时另一个线程通过该对象访问普通变量时,可能还没有赋值。(具体实例参看《Java并发编程的艺术》)

  ②  初次读一个包含final域的对象,与初次读这个final域之间不能重排序。防止对象引用提前读,而对象还未初始化完全

  在读对象引用和对象final域时,JMM在两者之间插入LoadLoad屏障,禁止读final域重排序到对象引用前边,普通变量可能存在这种情况。

  ③ 对final域是数组或对象等引用类型的情况,有如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后对该类对象的引用之间不能重排序。

  即final引用的对象中所有成员都写完成后,才可以被其他线程引用。

(2)禁止在构造函数中将this复制给外边的对象引用,否则可能导致其他线程看到初始化不完全的实例。

posted @ 2019-08-12 23:15  水木竹水  阅读(310)  评论(0编辑  收藏  举报