1 AbstractQueuedSynchronizer之AQS
1.1 先从字节跳动及其它大厂面试题说起
- 从集合开始吧,介绍一下常用的集合类,哪些是有序的,哪些是无序的
- hashmap是如何寻址的,哈希碰撞后是如何存储数据的,1.8后什么时候变成红黑树、说下红黑树原理有什么好处?
- reentrantlock实现原理,简单说下AQS
- synchronized实现原理,monitor对象是什么时候生成的?知道monitor和monitorenter和monitorexit这2个是怎么保证同步的吗,或者说底层是如何执行的
- 偏向锁和轻量级锁有什么区别?
1.2 前置知识
- 公平锁和非公平锁
- 可重入锁
- LockSupport
- 自旋锁
- 数据结构之链表
- 设计模式之模板设计模式
1.3 抽象的队列同步器
![](https://img.haomeiwen.com/i4639175/1b8eb7b73d47bd5f.png)
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer
通常地: AbstractQueuedSynchronizer简称为AQS
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态
![](https://img.haomeiwen.com/i4639175/62e9a00400ed92a2.png)
1.4 AQS为什么是JUC内容中最重要的基石
和AQS有关的
![](https://img.haomeiwen.com/i4639175/13ceedadbcd1d7aa.png)
ReentrantLock
![](https://img.haomeiwen.com/i4639175/c1868be697c87085.png)
CountDownLatch
![](https://img.haomeiwen.com/i4639175/d779779b5fcf9b4d.png)
ReentrantReadWriteLock
![](https://img.haomeiwen.com/i4639175/9f95770097de0d56.png)
Semaphore
![](https://img.haomeiwen.com/i4639175/f51822fdb4fbcc30.png)
进一步理解锁和同步器的关系
- 锁,面向锁的使用者
- 定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
- 同步器,面向锁的实现者
- 比如Java并发大神Douglee,提出统一规 范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
能干嘛
-
加锁会导致阻塞
- 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
1.5 AQS初步
1.5.1 AQS初识
![](https://img.haomeiwen.com/i4639175/f08e2e8dc2af4d8e.png)
有阻塞就需要排队,实现排队必然需要队列
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
![](https://img.haomeiwen.com/i4639175/6658357d24f76ba1.png)
1.5.2 AQS内部体系架构
![](https://img.haomeiwen.com/i4639175/b4e157369309d230.png)
AQS自身
-
AQS的int变量
- AQS的同步状态State成员变量
/** * The synchronization state. */ private volatile int; //银行办理业务的受理窗口状态 //零就是没人,自由状态可以办理 //大于等于1,有人占用窗口,等着
-
AQS的CLH队列
- CLH队列(三个大牛的名字组成),为一个双向队列
-
state变量+CLH双端Node队列
![](https://img.haomeiwen.com/i4639175/71da197da88265f6.png)
内部类Node(Node类在AQS类内部)
- Node的int变量
- Node的等待状态waitState成员变量 volatile int waitStatus
- 等候区其它顾客(其它线程)的等待状态
- 队列中每个排队的个体就是一个Node.
![](https://img.haomeiwen.com/i4639175/67d73cd67e086304.png)
![](https://img.haomeiwen.com/i4639175/00667ccb4165b01c.png)
1.5.3 AQS同步队列的基本结构
是用LockSupport.pork()来进行排队的
![](https://img.haomeiwen.com/i4639175/6877c9f5d58c2a85.png)
1.6 AQS详解(ReentrantLock开始解读AQS)
1.6.1 Code
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
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();
}
}
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
1.6.2 ReentrantLock原理
![](https://img.haomeiwen.com/i4639175/199b264451e77ee7.png)
1.6.3 lock方法的公平和非公平
通过ReentrantLock的源码来讲解公平锁和非公平锁
![](https://img.haomeiwen.com/i4639175/22696bfab163405a.png)
默认创建的是非公平锁
![](https://img.haomeiwen.com/i4639175/66bc5a4c62c68854.png)
可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
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());
}
对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors()
hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(), 之后还是需要竞争锁(存在线程竞争的情况下)
![](https://img.haomeiwen.com/i4639175/f8d6a112b76ed706.png)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.6.4 AQS源码深度分析
lock()
![](https://img.haomeiwen.com/i4639175/b037a7c8e6432542.png)
acquire()
![](https://img.haomeiwen.com/i4639175/15c5ddf09789bacf.png)
![](https://img.haomeiwen.com/i4639175/7bec2da9c5b1e412.png)
tryAcquire(arg) 非公平锁
![](https://img.haomeiwen.com/i4639175/618af99d822bbc0e.png)
![](https://img.haomeiwen.com/i4639175/cc0a1af81c6d14b7.png)
继续推进条件,走下一步方法addWaiter,否则结束。
addWaiter(Node.EXCLUSIVE)
![](https://img.haomeiwen.com/i4639175/a8a596d7a6d95735.png)
![](https://img.haomeiwen.com/i4639175/89d25c6c5e8d6edb.png)
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。
假如3号ThreadC线程进来,prev-compareAndSetTail-next
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
![](https://img.haomeiwen.com/i4639175/330387729afc15ca.png)
假如再抢抢失败就会进入shouldParkAterFailedAcquire和parkAndCheckInterrupt方法中
![](https://img.haomeiwen.com/i4639175/2735b71a478c97d5.png)
shouldParkAfterFailedAcquire
![](https://img.haomeiwen.com/i4639175/8f3a482725299141.png)
![](https://img.haomeiwen.com/i4639175/1e28c52779c5c9f8.png)
如果前驱节点的waitstatus是SIGNAL状态(-1),即shouldParkAfterFailedAcquire方法会返回true 程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起
parkAndCheckInterrupt
![](https://img.haomeiwen.com/i4639175/4a26b0056051cd5c.png)
方法unlock()
sync.release(1);
tryRelease(arg)
unparkSuccessor
![](https://img.haomeiwen.com/i4639175/a393e470785681d7.png)
1.6.5 AQS面试考点
你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?
3个状态:没占用是0,占用了是1,大于1是可重入锁
如果AB两个线程进来了以后,请问这个总共有多少个Node节点?
答案是3个
![](https://img.haomeiwen.com/i4639175/b4181d25a7cda62b.png)
网友评论