共计 2754 个字符,预计需要花费 7 分钟才能阅读完成。
看了下 AQS 的源码,有点简单,不适宜简略入门,我总结了下。
概述
Java 中的大部分同步类(Lock、Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer(简称为 AQS)实现的。AQS 是一种提供了原子式治理同步状态、阻塞和唤醒线程性能以及队列模型的简略框架。
从 ReentrantLock 独占模式看 AQS 原理
public void test () throw Exception {
// 初始化
ReentrantLock lock = new ReentrantLock(true);
// 加锁
lock.lock();
try {...} finally {lock.unlock();
}
}
lock()
看看非偏心锁的实现:
外围流程从这里开始:
1.compareAndSetState():线程进来间接利用 CAS
尝试抢占锁;setExclusiveOwnerThread():如果抢占胜利 state
值会被改为 1,且设置对象独占锁线程为以后线程
2.acquire(1):若利用 CAS
尝试抢占锁失败,也就是获取锁失败,则进入 Acquire 办法进行后续解决
Acquire 办法实现:
1.tryAcquire():再次尝试获取锁,如果加锁胜利则返回 true, 不再执行以下步骤,否则继续执行以下步骤
2.addWaiter():走到这里阐明加锁失败,创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中,而后返回这个 Node
3.acquireQueued():这个办法会先判断以后传入的Node
对应的前置节点是否为 head
节点,如果是则尝试加锁,如果加锁失败或者 Node
的前置节点不是 head
节点,用 LockSupport.park()
挂起以后线程。
上述流程图:
unlock()
1.tryRelease():state
被设置成 0,Lock 对象的独占锁被设置为 null
2.unparkSuccessor():唤醒 head
的后置节点,被唤醒的线程二会接着尝试获取锁,用 CAS
指令批改 state
数据。
上述流程图:
总结:AQS
中 保护了一个 volatile int state
(代表共享资源)和一个FIFO
线程期待队列(多线程争用资源被阻塞时会进入此队列)。
这里 volatile
可能保障多线程下的可见性,当 state=1
则代表以后对象锁曾经被占有,其余线程来加锁时则会失败,加锁失败的线程会被放入一个 FIFO
的期待队列中,比列会被 UNSAFE.park()
操作挂起,期待其余获取锁的线程开释锁才可能被唤醒。
另外 state
的操作都是通过 CAS
来保障其并发批改的安全性。
非偏心锁和偏心锁
当 线程二 开释锁的时候,唤醒被挂起的 线程三 , 线程三 执行 tryAcquire()
办法应用 CAS
操作来尝试批改 state
值,如果此时又来了一个 线程四 也来执行加锁操作,同样会执行 tryAcquire()
办法。这种状况就会呈现竞争,线程四 如果获取锁胜利,线程三 依然须要待在期待队列中被挂起。这就是所谓的 非偏心锁 , 线程三 辛辛苦苦排队等到本人获取锁,却眼巴巴的看到 线程四 插队获取到了锁。
非偏心锁 执行流程:
偏心锁在加锁的时候,会先判断 AQS
期待队列中是存在节点,如果存在其余期待线程,那么本人也会退出到期待队列尾部,做到真正的先来后到,有序加锁。
偏心锁 执行流程:
非偏心锁和偏心锁的区别:
非偏心锁性能高于偏心锁性能。非偏心锁能够缩小 CPU
唤醒线程的开销,整体的吞吐效率会高点,CPU
也不用取唤醒所有线程,会缩小唤起线程的数量
非偏心锁性能尽管优于偏心锁,然而会存在导致线程饥饿的状况。在最坏的状况下,可能存在某个线程始终获取不到锁。不过相比性能而言,饥饿问题能够临时疏忽,这可能就是 ReentrantLock
默认创立非偏心锁的起因之一了。
从 CountDownLatch 共享模式看 AQS 原理
void test() throws Exception {CountDownLatch latch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {System.out.println("线程 1 执行");
latch.countDown();}
}).start();
new Thread(new Runnable() {
@Override
public void run() {System.out.println("线程 2 执行");
latch.countDown();}
}).start();
latch.await();
System.out.println("线程 3 执行");
}
初始化
初始化 state 值,当 state 值 >0 代表锁被占有,= 0 阐明锁被开释
await()
1.tryAcquireShared():state 不等于 0 的时候,tryAcquireShared()返回的是 -1,此时获取锁失败,也就是说 count 未减到 0 的时候所有调用 await()办法的线程都要排队。
2.doAcquireSharedInterruptibly():创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中,先判断以后传入的 Node
对应的前置节点是否为 head
节点,如果是则尝试加锁,如果加锁失败或者 Node
的前置节点不是 head
节点,用 LockSupport.park()
挂起以后线程。
countDown()
1.tryReleaseShared():开释锁,通过自旋的 CAS 操作对 state-1,如果 state=0, 返回 true 执行 doReleaseShared()
2.doReleaseShared():唤醒期待 await()的线程
总结
独占模式流程:
1.tryRequire()办法尝试获取锁,具体通过 CAS 操作尝试批改 state 值,胜利则设置 state 值为 1,且设置对象独占锁线程为以后线程
2. 获取失败,创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中
3. 如果持有锁的线程应用 tryRelease() 开释了锁,state 从新设置为 0,独占线程设置为 null,唤醒队列中的第一个 Node 节点中的线程再次争抢锁
共享模式流程:
1.tryRequireShared()办法尝试获取锁,具体通过判断以后 state 值,>0 则代表获取锁失败,= 0 则获取锁胜利
2. 获取失败,创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中
3. 如果持有锁的线程应用 tryRelease() 开释了锁,会 state 进行 -1,当 state= 0 时,唤醒队列中所有的 Node 节点中的线程
参考大佬:
我画了 35 张图就是为了让你深刻 AQS
从 ReentrantLock 的实现看 AQS 的原理及利用