Java一面: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 操作更精准,可以准确地唤醒某一个线程
【免责声明:本文图片及文字信息均由千锋重庆Java培训小编转载自网络,旨在分享提供阅读,版权归原作者所有,如有侵权请联系我们进行删除。】