1. 前言
Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文会从应用层逐渐深入到原理层,并通过ReentrantLock的基本特性和ReentrantLock与AQS的关联,来深入解读AQS相关独占锁的知识点。
2. ReentrantLock
ReentrantLock支持公平锁和非公平锁,并且ReentrantLock的底层就是由AQS来实现的。那么ReentrantLock是如何通过公平锁和非公平锁与AQS关联起来呢?
2.1 code-1
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
private final Lock lock = new ReentrantLock();
// 从reentrant 分析 AQS
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock();
}
}
2.2 整体结构
2.3 通过ReentrantLock的源码看公平锁和非公平锁
- 公平锁和非公平锁的创建
- 实现区别比较
可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
- hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中,(前驱节点)
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}
看到这里,我们理解一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么?
双向链表中,第一个节点为虚节点(哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。
- 当h != t时:
- 如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True(这块具体见下边代码分析)。
- 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。
第二步判断:
- 如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同那么当前线程是可以获取资源的;
- 如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列
不懂了吧,我们来看AQS内部到底做了什么...
3. AbstractQueuedSynchronizer
AQS 是通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,
有一个int类变量(status)表示持有锁的状态,通过CAS完成对status值的修改(0表示没有,1表示阻塞)
- 应用领域:ReentrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore
3.1 同步器结构
-
锁:面向锁的使用者(定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可)
-
同步器:面向锁的实现者(比如Java并发大神Douglee,提出统一规 范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。)
-
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果
3.2 CLH队列(三个大牛的名字组成),为一个双向队列
释放锁后,会将原来的哨兵节点设置为null,help GC 回收,并将此头节点重新设置为哨兵节点
3.3 内部代码结构
(1) Node 节点
4. 通过lock/unlock作为案例突破口分析
前置知识
- AQS里面有个变量叫State,它的值有几种?3个状态:没占用是0,占用了是1,大于1是可重入锁
- 如果AB两个线程进来了以后,请问这个总共有多少个Node节点?答案是3个,其中队列的第一个是傀儡节点(哨兵节点)
lock() 方法调用链路
- lock()
- acquire()
- tryAcquire(arg)
- addWaiter(Node.EXCLUSIVE)
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
案例场景
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try{
System.out.println("-----A thread come in");
try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
}finally {
lock.unlock();
}
},"A").start();
//第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----B thread come in");
}finally {
lock.unlock();
}
},"B").start();
//第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----C thread come in");
}finally {
lock.unlock();
}
},"C").start();
}
}
4.1 lock() 方法
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 第一个线程抢占
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 第二个线程及后续线程抢占
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
4.2 acquire( ):源码和3大流程走向
(1)tryAcquire(arg)
- 本次走非公平锁方向
- nonfairTryAcquire(acquires):return false(继续推进条件,走下一步方法addWaiter),return true(结束)
(2)addWaiter(Node.EXCLUSIVE)
假如3号 ThreadC线程进来
(1). prev
(2).compareAndSetTail
(3).next
- addWaiter(Node mode )
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的
- enq(node);
- B、C线程都排好队了效果图如下:
(3)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
- acquireQueued :会调用如下方法:shouldParkAterFailedAcquire和parkAndCheckInterrupt | setHead(node) )
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 标记是否成功获取锁
try {
boolean interrupted = false; // 标记线程是否被中断过
for (;;) {
final Node p = node.predecessor(); // 获取前驱节点
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // // 获取成功,将当前节点设置为head节点
p.next = null; // help GC // 原head节点出队,在某个时间点被GC回收
failed = false; // //获取成功
return interrupted; // 返回是否被中断过
}
// 判断获取失败后是否可以挂起,若可以则挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- shouldParkAfterFailedAcquire
- parkAndCheckInterrupt
- 当我们执行下图中的③表示线程B或者C已经获取了permit了
- setHead( )方法
4.2 unlock() 释放锁
(1)release | tryRelease | unparkSuccessor(h);
-
tryRelease()
- unparkSuccessor( )
此时,带执行unlock() 后也就可以获得许可,自旋进入下一个节点的操作:
相关文章