Lock并发锁原理
# Lock并发锁原理
本章主要介绍:
- Java并发包中锁的源码实现
- 并发锁API
# Lock接口
| synchronized关键字 | Lock接口 | |
|---|---|---|
| 获取/释放锁 | 隐式 | 显示编程实现 |
| 可重入 | 支持 | ReentrantLock显式重入 |
| 灵活性 | 差 | 支持中断获取锁,超时获取锁 |
Lock常用的接口包括:
void lock():获取锁。
boolean tryLock():尝试非阻塞获取锁,立即返回。
boolean tryLock(long time):超时阻塞时获取锁。仅当①超时间内获取到锁②超时时间内被中断③到达超时时间三种情况下会返回。
# 队列同步器AQS
# 模板接口
同步状态
同步状态可以理解为同步器的锁资源,多线程竞争Lock的锁资源实际上是在竞争AQS的“同步状态”。
用一个int整数存储表示,语义上可以理解为锁资源的大小、允许的并发度。
AbstractQueuedSynchronizer(AQS)同步器提供了三个对“同步状态”操作的原子方法和查看锁占有线程的API:
- getState():获取当前同步状态
- setState(int newState):设置当前同步状态
- compareAndSetState(int current , int update):CAS更新当前状态。其中current表示当前getState结果。
- getExclusiveOwnerThread:返回当前独占锁的线程。
- setExclusiveOwnerThread(thread):设置当前占有锁的线程。
AQS抽象类主要面向锁的开发者,基于模板模式,所提供的方法可以分为两类:
- 可重写方法:包括tryAcquire(),tryRelease()等。基于上述三个原子操作对锁资源进行操作。
- 模板方法:同步器定义好可以直接拿来使用,包括acquire(),release()...模板方法会调用上面重写的方法。
实现的锁接口方法中,需要调用AQS模板方法实现同步。基于同步器框架可以实现同步组件,伪代码如下:
public class Mutex implements Lock {
static class Sync extends 同步器 {
//重写同步器方法
public boolean tryAcquire(int acquires){}
}
private final Sync sync=new Sync();
//外部暴露的锁方法。实际上都通过调用上述代理的同步器对象的方法实现
public void lock() { sync.acquire(1); }
public boolean tryLock() {return sync tryAcquire(1)}
public void unlock() {}
}
2
3
4
5
6
7
8
9
10
11
显然用户在使用Lock锁对象时,并不会直接和同步器打交道。锁Lock负责与用户交互使用,而底层代码实际上是基于同步器框架实现的。
# 同步器AQS源码实现
在AQS不同方法中,所谓死循环“自旋”分成以下两种:
- 模板方法:不断执行try方法尝试获取锁,根据try方法才能退出死循环
- 重写的try方法:锁可以获取的情况下重复执行CAS方法,直到成功修改同步状态位。无锁状态则不需要自旋。
# 1.同步队列

内部依赖于自定义的Node数据结构,它是一个FIFO双向同步队列,保存的节点是获取同步状态失败的线程引用。
同步器内维护队列的头节点和尾节点。队首node节点成功获取到同步状态后出队,并更新同步器的头节点。
- 队首元素更新不需要CAS,因为只会有一个元素能够获取同步状态。
- 队尾元素插入更新需要拿到CAS,因为同时会有多个元素同时竞争获取同步状态。
# 2.独占式同步状态获取与释放
独占式:同一时刻只能有一个线程成功获取同步状态。通过同步器的acquire原生方法实现,主要逻辑如下:
- 执行重写的同步器方法tryAcquire(),若获取失败则执行下面步骤。
- 构造同步节点,并通过CAS加入同步队列的尾部。
- 节点调用acquireQueued,进入自旋状态,以死循环+阻塞的方式尝试获取同步状态。只有前驱是头节点的节点才会尝试获取同步状态,从而保证FIFO的队列特性。以下两种情况被唤醒:
- 前驱节点出队列
- 阻塞线程被中断
线程acquire拿到锁,并返回执行完相应逻辑后,需要释放同步状态并唤醒后继节点。
# 3.共享式同步状态获取与释放
共享式:同一时刻可以有多个线程同时获取到同步状态。通过同步器的acquireShared原生方法实现。
共享式和独占式一样,如果锁获取失败,都会死循环自旋不断尝试获取同步状态。区别在于,共享式退出自旋的条件变为p.prev==head&&tryAcquireShared()>=0,重写方法tryAc返回值大于等于0表示能够获取到同步状态。
“同步状态”释放时,会有多个线程同时释放锁,因此需要保证线程安全。(独占式每次只有一个线程释放,不需要考虑线程安全)
# 4.独占式超时获取同步状态
同步器原生方法doAcquireNanos:核心逻辑与独占式和共享式类似。区别在于在自旋状态中,每轮循环会计算一次经过的时间间隔,并从超时时间nanosTimeout里面扣减。如果当前超时时间被扣减到小于0,则说明当前超时,直接返回退出。
若时间nanosTimeout比较短,则进入无条件的快速自旋。
# 案例——自定义同步组件
功能:同一时刻,只允许至多两个线程同时访问。超过两个线程的访问将会被阻塞。
基于上述API和接口,采用共享模式实现如下:
public class MyLock implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
public Sync(int count) {
if (count < 0) {
throw new IllegalArgumentException("count must larger than zero");
}
setState(count);
}
//返回更新后的同步状态值,小于0则表示获取失败
public int tryAcquireShared(int reduceCount){
while (true) {
int current = getState();
int newState = current - reduceCount;
//扣减失败,返回小于0的同步状态结果
if (newState < 0 ) {
return newState;
}
//大于0,则执行CAS成功后才进行扣减
else{
if(compareAndSetState(current, newState))
return newState;
}
}
}
public boolean tryReleaseShared(int returnCount){
while (true) {
int current = getState();
int newState = current + returnCount;
if (compareAndSetState(current, newState)) {
return true;
}
}
}
}
private static Sync sync = new Sync(2);
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void unlock() {
sync.releaseShared(1);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# ReentrantLock重入锁
ReentrantLock锁主要包含如下特性:
ReentrantLock是一个独占锁(排他锁),仅允许一个线程同时持有锁。
ReentrantLock的同步状态state语义上相当于一个计数器,代表当前占有锁的线程重入的次数。
- state=0:代表当前锁没有线程占用,任何线程可以获取锁,需要CAS抢占锁。
- state=n:代表锁被某个线程重入了n次。
重入性:占有锁的线程支持再次调用lock方法,重新获取锁进入同步块。
公平性:先申请获取锁的请求优先被满足,获取锁的过程是顺序的。
# 1.重入性
重入性实现核心是通过getExclusiveOwnerThread判断当前线程是否为占有锁的线程,分为获取和释放两部分:
- 获取锁逻辑:首先判断当前同步状态,如果为0则直接抢占锁;若大于零,则判断线程是否为占用锁的线程,如果是同一个线程,累加同步状态计数器。
- 释放锁逻辑:同步状态计数器扣减当前释放线程数,当且仅当同步状态减到0才算真正的释放完毕,将锁占有线程置为空并返回true。
# 2.公平锁
ReentrantLock默认实现是非公平锁。公平锁能够实现按照“请求顺序”获取锁,与AQS内部队列顺序保持一致。
公平锁请求顺序
“请求顺序”是指多线程调用tryAcquire的顺序,它不等于AQS内部等待队列的顺序,也不等于代码执行顺序。
无论是公平锁还是非公平锁,如果所有线程都进入了等待队列,那么都必须按照AQS内部等待队列的顺序执行。
当前可以获取锁时,CAS操作多加hasQueuedPredecessors()判断条件,如果当前节点有前驱节点返回true,保证每个能够获取锁的一定是头结点,没有插队的线程。
if(!hasQueuedPredecessors() && compareAndSetState() )
而非公平锁,某一个时刻还没加入等待队列时tryacquire,如果当前正好锁释放了可以获取同步状态,那么当前线程无需加入等待队列阻塞,直接拿锁返回,CPU无需唤醒其它线程。
# 3.公平锁和非公平锁对比
💡对比实验需要死循环调用获取锁。若所有线程都进入同步队列,那么结果毫无意义,无法区分是否公平。
- 线程饥饿现象
非公平锁中,刚释放完锁的线程即使处于队列尾部,也可能会立刻重新获取到锁,从而导致其它线程一直不能调度,出现”饥饿“的情况。
而在公平锁中,刚释放的锁排在队尾,必定不能立刻重新获取锁。
- 上下文开销
虽然非公平锁不能调度,但连续两次线程获取到线程,不需要进行上下文切换,减少系统开销,吞吐和效率更高
而反过来,公平锁实现FIFO的代价就是,进行大量上下文切换,开销大。
# ReentrantReadWriteLock读写锁
读锁多个线程同时访问,写锁则只允许一个线程。两种实现方式:
- 基于wait/notify的等待通知机制,但是代码编程复杂
- ReentrantReadWriteLock读锁和写锁,简单明了。
# 接口示例
ReadWriteLock提供的API主要分为两类:
- 获取读写锁:readLock(),writeLock(),返回Lock接口的引用,通过调用读写锁的lock,unlock方法实现读写并发控制。
- 监控内部读写锁状态的方法:包括getReadLockCount(),isWriteLocked()...获取读锁次数,写锁是否获取。
# 源码分析
# 1.读写同步状态划分

将同步状态按位切割,分别存储“读锁”的同步状态,“写锁”的同步状态:
- 写锁同步状态:state=0代表当前未获取,state>0代表线程重入的次数。
- 读锁同步状态:state作为计数器记录当前获取读锁的线程数。
getState>0,写锁同步状态为0,那么可以推出一定有读锁同步状态大于0,当前处于读状态。
# 2.写锁获取与释放
写锁抢占获取逻辑如下:
- 如果读写同步状态大于0,分别判断读同步状态和写同步状态:
- readState>0,则获取写锁失败。
- writeState>0,则进行重入性判断,当前线程不是重入线程则失败。否则获取写锁成功。
- 同步状态等于0,则需要CAS占用锁。
写锁释放逻辑与ReentrantLock锁释放逻辑相似。
# 3.读锁获取与释放
读锁获取逻辑:只要写状态为0,读状态大于0,则读锁总会被成功获取。另外如果当前线程获取了写锁,那么当他尝试获取读锁时,也能成功获取(降级)。
读同步状态指的是,所有读线程重入读锁的次数之和。每个线程单独维护一个ThreadLocal对象,保存当前线程重入的次数之和。
读锁释放与ReentrantLock释放类似,需要CAS控制并发。
# 4.锁降级
锁降级指的是并发情况一种数据可见性控制策略,适用于特定的场景。
- 获取持有写锁
- 尝试获取读锁
- 释放写锁
其中中间这一步获取读锁,目的是为了保证数据的可见性。当前线程释放写锁后还持有读锁,这使得其它阻塞在写锁的线程仍然处于被阻塞的状态。相当于延长了当前线程对数据一致性的控制周期。
# LockSupport工具
定义了一组公共静态方法,用于控制线程阻塞和唤醒:
| 方法名称 | 描述 |
|---|---|
| park() | 阻塞当前线程 |
| unpark(thread) | 唤醒处于阻塞状态的thread线程 |
| parkNanos(blocker,nanos) | blocker标识当前线程阻塞对象 |
# Condition接口
监视器方法用于在锁同步块内实现线程的等待通知机制,目前有两种实现方式:
- Object对象的监视器方法:wait,notify, notifyAll。与synchronized配合
- Condition接口监视器方法:await。与Lock.lock配合
# 接口示例
Condition对象在await()阻塞和signal()通知线程时,必须先获取与其绑定的Lock锁对象。
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public void conditionAwaitOrSignal() throws InterruptedException {
lock.lock();
try {
condition.await();//阻塞当前线程
//condition.signal(); 其它线程调用唤醒当前线程
} finally {
lock.unlock();
}
}
2
3
4
5
6
7
8
9
10
11
和先前等待通知机制的三段式相同,阻塞代码需要使用while而不是if,防止过早或者意外通知。
while (count==queue.length) addCondition.await();
# 源码分析
ConditionObject是AQS同步器的内部类,每个Condition对象都维护一个FIFO“等待队列”,每个节点都是在该Condition对象上等待的线程。

在AQS同步器+Condition模型的监视器模型中包含:
- 一个AQS同步队列
- 多个Condition等待队列

底层无论是await方法,还是signal方法,都用到了LockSupport进行阻塞和唤醒节点线程:
- await():线程被Condition.await方法阻塞时,相当于将当前线程从AQS同步队列的头结点,移动到了Condition等待队列当中。
- signal():从Condition等待队列头节点移除,加入到同步队列中。再使用LockSupport唤醒(跳出await方法的while循环),继续进行锁的竞争。
- signalAll():所有等待队列的节点都执行一次signal,移动到同步队列。