面试官:AQS 了解吗,讲一讲
我:.......告辞了
这是一个老生常谈的面试题,相信大家都可能会碰到过。
有关这一块资料其实网上一搜便是一堆,今天肥壕主要是想结合自己的理解,用更加通俗易懂的方式表达出来,也不涉及任何的源码。
实现原理
AQS(AbstractQueuedSynchronizer),抽象的队列式同步器
AQS 维护了一个 state
(共享资源变量)和一个 FIFO
线程等待队列(CLH 队列),多个线程竞争 state
被阻塞时就会进入此队列中。
State
state
是用 volatile 修饰的一个 int 类型的共享资源变量
资源共享的两种方式:
- Exclusive:独占,只有一个线程能执行,如 ReentrantLock
- Share:共享,多个线程可以同时执行,如 CountDownLatch、CyclicBarrier、Semaphore、ReadWriteLock
CLH 队列(FIFO)
简短说就是一个双向链表,使用内部类 Node 来实现的。head、tail 指针分别指向链表的头部和尾部。
我们一般常用的写法如下:
ReentrantLock lock = new ReentrantLock();
// 加锁
lock.lock();
// 业务逻辑代码
...
// 解锁
lock.unLock();
场景分析
那在加锁和解锁的具体过程究竟是怎么样的呢,肥壕举了两个比较简单的例子
加锁
线程 A、B、C 同时抢占锁,此时线程 B 抢占成功,线程 A、C 则失败,具体流程如下:
- 线程 B 抢占锁的过程中把
state
通过 cas 更新为 1。 - 线程 A、C 因为更新失败,所以也就抢占失败。
- 抢占锁失败的线程,都会被放入到一个 FIFO 的线程等待队列中(双向链表)。
- head、tail 分别指向队列的头和尾。
解锁
此时线程 B 执行完业务逻辑后,调用 lock.unlock()
,具体流程如下:
- 线程 B 通过 cas 把
state
更新为 0 - 唤醒等待队列中 head 的下一个节点线程 A
公平锁与非公平锁
这也是平时面试经常被问到的一个问题,这里简要谈一谈
-
公平锁:按照队列中的等待顺序,依次取队头的线程。比如上面的例子中,下一个获取锁的线程一定是线程 A
-
非公平锁:在释放锁后,如果有新的线程尝试获取锁,有可能会抢占成功。比如在线程 B 释放锁的瞬间,有个新的线程 D,尝试获取锁,有很大几率会抢占成功。
具体相关代码可以看 ReentrantLock
下的两个静态类 FairSync
、NonFairSync
Condition
ReentrantLock 中可以通过 newCondition()
方法创建一个 Condition 对象,那这个对象究竟是啥玩意呢?
简单说,替代传统的 Object 的 wait()、notify() 实现线程间的协作。
先来看一个使用实例:
public class Demo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void methodAwait() {
try {
lock.lock();
System.out.println(String.format("### 当前线程:%s waiting ###", Thread.currentThread().getName()));
condition.await();
System.out.println(String.format("### 当前线程:%s finished ###", Thread.currentThread().getName()));
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void methodSignal() {
try {
lock.lock();
System.out.println(String.format("### 当前线程:%s signal ###", Thread.currentThread().getName()));
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread t1 = new Thread(() -> demo.methodAwait(),"thread-A");
Thread t2 = new Thread(() -> demo.methodAwait(), "thread-B");
Thread t3 = new Thread(() -> demo.methodAwait(), "thread-C");
Thread t4 = new Thread(() -> demo.methodSignal(), "thread-D");
t1.start();
t2.start();
t3.start();
Thread.sleep(2000);
t4.start();
}
}
线程 A、B、C、D 同时启动抢占锁,这时抢占成功的线程会执行自己的逻辑业务,抢占失败的就会像上面所说,进入线程CLH 队列中。
假设线程 B 先获取锁,调用 condition.await()
方法后释放锁,阻塞并进入条件等待队列,线程 A 、C 获取锁后也依次进入条件等待队列。
线程 D 获取锁后调用 condition.signalAll()
方法,它会将条件等待队列中的线程放入 CLH 队列,并唤醒所有的等待线程。
注意,条件队列中的线程是依次一个一个加入 CLH 队列的队尾。
扩展
LockSupport
在 AQS 中,队列中线程的阻塞唤醒都是通过 LockSupport 实现的。
LockSupport 类,是用来创建锁和其他同步类的基本线程阻塞原语,核心方法只要看这两个:
park()
:阻塞当前调用线程unpark()
:唤醒指定线程
相比Object 类中的 wait()、notify()、notifyAll(),区别是:
- wait/notify/notifyAll 必须在 synchronized 中使用
- LockSupport 操作更精准,可以准确地唤醒某一个线程
普通的改变,将改变普通
我是宅小年,一个在互联网低调前行的小青年
关注公众号「宅小年」,个人博客 📖 edisonz.cn,阅读更多分享文章