《Java并发编程的艺术》之synchronized及JUC

synchrnoized的happens-before

int a;
boolean flag;
public synchronized void init(){// ①
    a = 100; // ②
    flag = true; // ③
}// ④
public synchronized void doTask(){ // ⑤
    if(flag){ // ⑥
        int result = a; // ⑦
    }
} // ⑧

假设线程A执行init(),线程B执行doTask(),有如下的happens-before关系:

  • 根据程序次序规则:
    • ① hb ②
    • ② hb ③
    • ③ hb ④
    • ⑤ hb ⑥
    • ⑥ hb ⑦
    • ⑦ hb ⑧
  • 根据监视器规则:
    • ① hb ④
    • ④ hb ⑤
    • ⑤ hb ⑧

根据传递规则,保证init()方法所有的修改对doTask()方法可见,happens-before关系如下所示

1、2间的表示程序次序性规则,4、5间的表示监视器规则,由于3、4有happens-before关系,4、5有happens-before关系,所以根据传递性规则,2、6间有happens-before关系。

线程A释放锁之前所有可见的共享变量,在线程B获取同个锁之后就变得可见了。

synchrnoized的内存语义

synchrnoized获取锁内存语义

当释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主内存。以上面的例子为例,共享数据的状态示意图如下所示

synchrnoized释放锁内存语义

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

语义总结

对比锁释放-获取的内存语义和volatile的写-读内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取和volatile读有相同的内存语义。

  • 线程A释放一个锁,实质上是告诉其他 要获取该共享变量 线程 一个消息(线程A所做的修改)
  • 线程B获取一个锁,实质上是接受到其他线程发出的消息(释放这个锁的线程对共享变量所做的修改)
  • 线程A释放,随后线程B获取,实质上是线程A通过主内存给线程B发送消息。

这里判断语义是否相同是通过两个操作的流程是否相同,比如线程A的锁释放完时,刷新至主内存;volatile写完后,刷新至主内存,并通知其他线程本地内存的共享变量失效(在锁释放环节里是交给锁获取执行);

CAS和JUC

synchronized是通过控制对象头来控制锁的升级,但是具体获取锁和释放锁的流程藏在JVM里,这里将通过ReentrantLock类比synchronized过程。

ReentrantLock的实现流程

扫描二维码关注公众号,回复: 6332616 查看本文章

这里要学习的是CAS,JDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步设置为给定的更新值。此操作具有volatile读和写的语义。
前面讲到volatile写保证volatile写不会和前面的操作发生重排序,volatile读保证volatile读不会和后面的操作发生重排序。组合这两个条件就意味着同时实现了 禁止某一操作和操作前、操作后的重排序。CAS操作就是如此,它在是通过加上lock前缀来实现以下的功能:

  • 使用缓存锁定保证原子性
  • 禁止之前和之后的重排序
  • 把写缓冲区中的所有数据刷新到内存

正是因为CAS同时具有volatile读和写的内存语义,因此Java线程之间的通信有下面四种方式。

  1. A线程写volatile变量,B线程读这个volatile变量
  2. A线程写volatile变量,B线程用CAS修改volatile变量
  3. A线程用CAS修改volatile变量,B线程用CAS修改这个变量
  4. A线程用CAS修改volatile变量,B线程用volatile读取该变量

JUC包的通用化的实现模式:

  • 声明共享变量为volatile
  • 使用CAS的原子条件来实现线程间的同步
  • 配合volatile读/写和CAS 来实习线程间的通信

AQS,非阻塞数据结构和原子变量类,这些JUC包中的基础都是使用上面的模式来实现的,而JUC包的高层类又是依赖这些基础类来实现的。从整体看,JUC包的实现示意图如下所示。

猜你喜欢

转载自www.cnblogs.com/codeleven/p/10963109.html