JMM: 锁的内存语义

锁是Java并发编程中最重要的同步机制,锁除了可以让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

锁释放-获取内存语义

锁的释放内存语义:线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

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

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

锁内存语义的实现

锁的内存语义实现依赖两种方式:

  1. 利用volatile变量的写-读所具有的内存语义。
  2. 利用java的CAS所附带的volatile读和volatile写的内存语义。

CAS

Java中的compareAndSet()方法调用简称为CAS,JDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

此操作具有volatile读和写的内存语义。

CAS如何具有volatile的读-写内存语义?

  1. 和volatile内存语义实现类似,编译器不会对CAS和CAS前后的任意内存操作进行重排序。
  2. 处理器对CAS的支持
    CAS操作底层是进行本地方法调用,本地方法的实现(以Windows和Intel X86处理器为例)会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序运行在多处理器上,就为cmpxchg指令加上lock前缀;反之,如果程序运行在单处理器上,就省略lock前缀。
    intel对lock前缀的说明:
    1. 确保对内存的读-改-写操作原子性执行。Intel使用缓存锁定(Cache Locking)来保证指令执行的原子性。
    2. 禁止该指令,与之前和之后的读和写指令重排序。
    3. 把写缓冲区的所有数据刷新到内存中。

CAS 还是 volatile?

为什么有了 volatile 还要有 CAS?
volatile关键字虽然具有原子性,但是 volatile 变量的复合操作是不具有原子性的,例如自增操作,所以volatile关键字的应用场景受限,而 CAS 可以解决这种复杂的场景,提供原子性。

concurrent包的实现

volatile变量的读/写和CAS可以实现线程之间的通信,这是整个concurrent包得以实现的基石。

cocurrent包一个通用的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的的内存语义来实现线程间的通信。

concurrent包的实现

AQS(Java的同步器框架AbstractQueuedSychronized),非阻塞数据结构和原子变量类(java.util.concurrent.atomic包),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类实现的。