面试官:AQS了解吗?

面试官:AQS了解吗?

Scroll Down

面试官: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 下的两个静态类 FairSyncNonFairSync

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(),区别是:

  1. wait/notify/notifyAll 必须在 synchronized 中使用
  2. LockSupport 操作更精准,可以准确地唤醒某一个线程

普通的改变,将改变普通

我是宅小年,一个在互联网低调前行的小青年

关注公众号「宅小年」,个人博客 📖 edisonz.cn,阅读更多分享文章