推荐阅读
- 学习笔记 《 深入理解 Java 虚拟机》
- 学习笔记 《 后端架构设计》
- 学习笔记 《 Java 基础知识进阶》
- 学习笔记 《 Nginx 学习笔记》
- 学习笔记 《 前端开发杂记》
- 学习笔记 《 设计模式学习笔记》
- 学习笔记 《 DevOps 最佳实践指南》
- 学习笔记 《 Netty 入门与实战》
- 学习笔记 《 高性能MYSQL》
- 学习笔记 《 JavaEE 常用框架》
- 学习笔记 《 Java 并发编程学习笔记》
- 学习笔记 《 分布式系统》
- 学习笔记 《 数据结构与算法》
Lock的内存语义
众所周知,锁可以让临界区互斥执行。这两将介绍锁的另一个同样重要,但是常常被忽视的功能:锁的内存语义,了解锁的内存语义,将是我们对锁的认识更加的深刻。
1、锁所建立的 happens-before 关系
锁是Java并发编程中最重要的同步机制,锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息,下面以一个示例程序进行讲解
class MonitorExample{
int a=0;
public synchronized void writer(){ // A
a++; // B
}
public synchronized void reader(){ // C
int i=a; // D
...
}
}
假设线程1执行writer()方法,随后线程2执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类
- 根据程序次序规则,A happens-before B,C happens-before D;
- 根据监视器规则,A happens-before C
- 根据happens-before的传递性,B happens-before D
在线程1释放了锁之后,随后线程2获取同一个锁。因此线程1在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。
2、锁的释放与建立的内存语义
- 锁释放的内存语义:当线程释放锁时,JMM会把该线程对应的(所有)本地内存中的共享变量刷新到主内存中
- 锁获取的内存语义:当线程获取锁时,JMM会把该线程对应的(所有)本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
- 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所作修改的)消息
- 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所作修改的)消息
线程A释放锁,随后线程B获取锁,这个过程实质上是线程A通过主内存向线程B发送消息,对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义,锁获取与volatile读有相同的内存语义。
3、锁的内存语义的实现
**正是由于JDK 1.5之后volatile的内存语义得到增强,才可能实现Java concurrent包,**借助ReentrantLock的源代码,来分析锁内存语义的具体实现机制,请看下面代码
class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock();//获取锁
try {
a++;
} finally {
lock.unlock();//释放锁
}
}
public void reader() {
lock.lock();//获取锁
try {
int i = a;
...
} finally {
lock.unlock();//释放锁
}
}
}
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(简称为AQS),关于AQS的相关知识点请参考笔者在本系列的其他文章,这里不做过多的赘述。
AQS使用一个整型的 volatile 变量(state)来维护同步状态,这个volatile变量是实现ReentrantLock内存语义实现的关键。
所以锁在释放的时候实际上是修改的 state 这个 volatile 变量,从而达到了锁在释放时候修改volatile变量刷新主内存;在锁获取读 state 这个 volatile 变量从而达到在获取锁的时候更新本地内存的效果。这也是为什么锁的释放的内存语义等效于 volatile 变量的修改,锁的获取的内存语义等效于 volatile 的读取的根本原因。
其次这里对 state 变量的修改使用的是Unsafe 类中的 compareAndSwapInt() 方法,这个方法声明为native 方法,我们可以参看其 C++ 源码。
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value){
int mp = os::is_MP(); // 检查系统是否是多个处理器 MultipleProcessor
__asm{
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp) //如果是多个处理器,那么在操作之前添加 LOCK 指令
cmpxchg dword ptr [edx], ecx
}
}
Lock指令在Intel的CPU中有以下作用: 1. 确保读-改-写操作原子化执行 2. 禁止该指令与之前和之后读写指令重排序。3. 把写缓冲区的数据刷新到主内存中。 所以根据2 & 3 的作用可以得知,CAS具有volatile 写和读的内存语义。
所以我们可以看到锁的内存语义的实现范式有两种: 一种是对 volatile 变量的修改和读取 另外一种是使用和前者具有相同内存语义的的 CAS 操作, 实际上对 Synchronized 实现的锁,其也会在指令前添加LOCK指令,从而实现锁的内存语义