锁可以让临界区互斥执行。
锁的释放和获取的内存语义
当线程释放锁时,JMM(Java内存模型)把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
对比锁释放-获取的内存语义与volatile
写-读的内存语义可以看出:锁释放与volatile
写有相同的内存语义;锁获取与volatile
读有相同的内存语义。
- 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
- 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做的修改的)消息。
- 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
锁内存语义的实现
在ReentrantLock
中,调用lock()
方法获取锁;调用unlock()
方法释放锁。
学习笔记:独占锁ReentrantLock的原理
ReentrantLock公平锁及非公平锁实现
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer
(AQS)。
AQS使用一个整型的volatile变量来维护同步状态:private volatile int state;
公平锁:加锁方法lock()
调用轨迹:
- ReentrantLock:lock()
- FairSync:lock()
- AbstractQueuedSynchronizer:acquire()
- ReetrantLock$FairSync:tryAcquire()
加锁方法首先读volatile变量state:int c = getState();
公平锁:解锁方法unlock()
调用轨迹:
- ReentrantLock:unlock()
- AbstractQueuedSynchronizer:release()
- ReetrantLock$Sync:tryRelease()
在释放锁的最后写volatile变量state:setState(c);
公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量state。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。
非公平锁:加锁方法lock()
调用轨迹:
- ReentrantLock:lock()
- NonfairSync:lock()
- AbstractQueuedSynchronizer:compareAndSetState()
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
该方法以原子操作的方式更新state变量。
JDK文档对CAS方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义
编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state
- 公平锁获取时,首先会去读volatile变量
- 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。
从以上对ReentrantLock的分析可以看出,锁释放-获取的内存语义实现至少有下面两种方式:
- 利用volatile变量的写-读所具有的内存语义。
- 利用CAS所附带的volatile读和volatile写的内存语义。
《Java并发编程的艺术》