- 1.前言
- 2.ReentrantLock介绍
- 3.AQS
- 3.1主要原理
- 3.2数据结构
- 3.3不同模式加锁过程
- 3.4 自定义同步器中需要实现的重要方法
- 4 ReentrantLock中的AQS分析
- 4.1加锁过程
- 4.1.1第一步:尝试获取锁(利用cas更改state值)
- 4.1.2第二步:线程加入队列
- 4.1.3第三步:队列中的节点自旋并阻塞
- 4.2解锁过程
- 4.2.1 第一步:资源释放
- 4.2.2 第二步,如果资源全部释放则唤醒阻塞的线程
- 4.3 Condition 实现原理
- 4.3.1 condition队列数据结构
- 4.3.2 await方法与signal方法执行逻辑
- 参考文章
1.前言
AQS位于java.util.concurrent.locks包下,全名 AbstractQueuedSynchronizer,本质上是一套同步器框架,java中许多线程同步工具都是基于它的。
| 工具 | 是什么 |
|---|
ReentrantLock | ReentrantLock是可重入的排它锁,同一时刻仅有一个线程可以进行访问 |
| ReentrantReadWriteLock | ReentrantReadWriteLock维护着一对锁,一个读锁,一个写锁,同一个时间,允许多个线程同时获取读锁 |
| CountDownLatch | 使用给定计数初始化CountDownLatch,由于调用countDown方法,在当前计数到达0之前,await方法会一直阻塞, 之后会释放等待线程计数无法被重置 |
| Semaphore | 信号量Semaphore是一个控制访问共享资源的计数器,信号量初始化许可的个数即是当前允许线程同时访问总的个数。
在获取许可前,会阻塞acquire的线程。每一个release会添加一个许可,并可以释放一个正在阻塞的获取者。 |
本篇将以使用最多的ReentrantLock为例,进行AQS原理分析
在正式进入介绍之前先理解一下几个常见的锁概念:
公平锁与非公平锁
公平锁:公平锁中,锁的获取顺序是按照先到先得的原则进行的。实现上,某个线程竞争锁时,只要锁的等待队列不为空,则该线程需要阻塞插入队列尾部,获取竞争锁的机会
非公平锁:后到的新线程可以直接竞争锁,只有在竞争锁失败时,才会被阻塞插入队列尾部。
共享锁与排他锁
共享锁与排他锁最主要的区别就是:排他锁在同一时刻只能被同一个线程占有,而共享锁可被多个线程持有。
可重入锁
可重入锁支持被同一个线程多次获取,即当一个线程获取锁之后,这个线程还可以再次获取锁
我们通过AQS都可以实现上述的公平锁,非公平锁,共享锁,排他锁,可重入锁,那么怎么实现呢,稍后一一介绍
2.ReentrantLock介绍
ReentrantLock是一种可重入排他锁
类图:
如上图,ReentrantLock基于Sync内部类实现了Lock接口,其中Sync实现了AQS,提供了NonfairSync和FairSync 2种实现。
ReentrantLock与Synchronized区别
| 特征 | ReentrantLock | Synchronized |
|---|
| 功能 | 支持响应中断,超时,尝试获取 | 不支持 |
| 锁类型 | 公平锁,非公平锁 | 非公平锁 |
| 实现 | AQS | jvm监视器 |
| 重入性 | 可重入 | 可重入 |
| 解锁 | 手动解锁 | 自动解锁 |
非公平锁的加锁流程
| final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } |
通过CAS设置status
- 如果成功的话,说明获取锁成功,并将独占线程设为当前线程
- 失败的话,说明获取锁失败,调用acquire(1)
公平锁的加锁流程
| final void lock() { acquire(1); } |
公平锁中,直接调用acquire(1)方法
公平锁与非公平锁的解锁流程是一样的
| public void unlock() { sync.release(1); } |
由上可以看出,在加锁流程中,不论是公平锁还是非公平锁,都调用了acquire()方法以及解锁流程中调用的release方法,正是在AQS中定义的核心方法
3.AQS
3.1主要原理
AQS主要数据结构为双向链表+state
主要原理如下:
线程1请求获取资源state空闲,将线程1设置为工作线程,并锁定资源。而其他线程请求获取资源时发现资源被占用,AQS就将请求资源的线程阻塞并封装成节点入FIFO队列(尾部入队列,头部出队列),当线程1执行完毕,完全释放所有资源后,AQS会唤醒线程2,使得线程2获得竞争资源的机会,以此往复。。。
3.2数据结构
这里的数据结构,是指队列中的节点
| 类型 | 属性 | 描述 |
|---|
volatile Node | prev | 前驱节点 |
volatile Node | next | 后继节点 |
volatile Thread | thread | 处于该节点的线程 |
volatile int | waitStatus | 节点在队列中的状态 CANCELLED 1:表示由于线程超时或中断导致的线程获取锁的请求已经取消了 SIGNAL -1:线程准备好,等待资源释放 CONDITION -2:节点在condition队列中,节点线程等待唤醒 PROPAGATE -3:线程处于SHARED(共享模式)时,会使用 0:Node被初始化的默认值 |
| | |
Node | nextWaiter | 返回处于CONDITION状态的节点 |
| SHARED /EXCLUSIVE | SHARED :共享模式等待锁 EXCLUSIVE:独占模式等待锁 |
volatile int | state | 临界资源 |
3.3不同模式加锁过程
3.4 自定义同步器中需要实现的重要方法
| 方法名称 | 描述 |
|---|
tryAcquire(int) | 独占方式,尝试获取资源,成功返回true,失败返回false |
tryRelease(int) | 独占方式,尝试释放资源,成功返回true,失败返回false |
tryAcquireShared(int) | 共享方式,尝试获取资源 |
tryReleaseShared(int) | 共享方式,尝试释放资源 |
其中tryAcquire-tryRelease是一对、tryAcquireShared-tryReleaseShared是一对,可以只实现其中的一对,如ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease
4 ReentrantLock中的AQS分析
4.1加锁过程
以非公平锁为例进行代码分析
4.1.1第一步:尝试获取锁(利用cas更改state值)
如果成功则获取锁,失败则进行acquire(1)
| static final class NonfairSync extends Sync { ... final void lock() { //直接尝试获取锁, if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } ... } |
获取锁失败
| public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } |
非公平锁中tryAcquire()实现,如果返回true则获取锁,返回false,则需要加入队列
| protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //c==0说明队列为空 if (c == 0) { //尝试独占资源 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //可重入锁逻辑,判断独占资源的线程是否就是当前的线程 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc 0说明是取消状态,循环向前查找,把取消的节点删除 if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ //这时waitStatus 只剩下0或者PROPAGATE compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } |
阻塞
| private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } |
4.2解锁过程
ReentrantLock在解锁的时候,并不区分公平锁和非公平锁
4.2.1 第一步:资源释放
| public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } //tryRelease中资源全部释放返回true,否则返回false protected final boolean tryRelease(int releases) { //减少可重入次数 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //如果资源占用全部释放,则将独占线程释放 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } |
4.2.2 第二步,如果资源全部释放则唤醒阻塞的线程
| public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; // if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } |
| private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; } |
待续。。。
参考文章
https://segmentfault.com/a/1190000016462281
从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队