1、为什么学AQS?
理解其背后的原理、学习设计思想,以提高技术并应对面试
1.1、AQS 的重要性
我们先来介绍一下 AQS(AbstractQueuedSynchronizer)的重要性,来看看 AQS 被用在了哪些类里面。
如图所示,AQS 在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS 是这些类的底层原理。
2、这个能解决什么问题?
3、AQS是什么?
4、怎么用?
5、为什么用?
6、什么原理?
我们对 AQS 进行内部原理解析的话需要抓住重点,因为 AQS 的内部比较复杂,代码很长而且非常不容易读懂,如果我们一上来就一头扎进去读源码,是很难完全掌握它的。所以在本课时中,我们把 AQS 最核心的三个部分作为重点提炼出来,由这三个部分作为切入点,打开 AQS 的大门。
AQS 最核心的三大部分就是状态、队列和期望协作工具类去实现的获取/释放等重要方法。
一、state 状态
第一个要讲解的是状态 state,如果我们的 AQS 想要去管理或者想作为协作工具类的一个基础框架,那么它必然要管理一些状态,而这个状态在 AQS 内部就是用 state 变量去表示的。它的定义如下:
/** * The synchronization state. */ private volatile int state;
而 state 的含义并不是一成不变的,它会根据具体实现类的作用不同而表示不同的含义,下面举几个例子。
比如说在信号量(Semaphore)里面,state 表示的是剩余许可证的数量。如果我们最开始把 state 设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state 就会变为 9,所以信号量的 state 相当于是一个内部计数器。
案例一:
public class SemaphoreTest { public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(10,true); //线程1 new Thread(new Runnable() { @Override public void run() { try { semaphore.acquire();//获取信号灯许可 } catch (InterruptedException e) { e.printStackTrace(); } //availablePermits(): Returns the current number of permits available in this semaphore. //返回此信号量中可用的当前许可数。 System.out.println("semaphore.release():"+semaphore.availablePermits()); } }).start(); //线程2 new Thread(new Runnable() { @Override public void run() { semaphore.release();//释放信号灯 System.out.println("semaphore.release():"+semaphore.availablePermits()); } }).start(); } }
注意:另外需要注意的一点是,信号灯可以由一个线程使用,然后由另一个线程来进行释放,而锁只能由同一个线程启动和释放,不然就好发生死锁,这一点需要格外注意。
打印结果:
semaphore.release():9
semaphore.release():10
再比如,在 CountDownLatch 工具类里面,state 表示的是需要“倒数”的数量。一开始我们假设把它设置为 5,当每次调用 CountDown 方法时,state 就会减 1,一直减到 0 的时候就代表这个门闩被放开。
CountDownLatch:它是一个同步工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。
CountDownLatch的两种使用场景:
- 场景1:让多个线程等待。模拟并发的场景
- 场景2:让单个线程等待。主线程等待,直到count为0的时候,唤醒主线程继续执行
案例一:
public class CountDownLatchTest { public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(2); System.out.println("主线程开始执行…… ……"); //第一个子线程执行 ExecutorService es1 = Executors.newSingleThreadExecutor(); es1.execute(new Runnable() { @Override public void run() { try { Thread.sleep(3000); System.out.println("子线程:" + Thread.currentThread().getName() + "执行" + latch.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } /** * Decrements the count of the latch, releasing all waiting threads if * the count reaches zero. * * <p>If the current count is greater than zero then it is decremented. * If the new count is zero then all waiting threads are re-enabled for * thread scheduling purposes. * * <p>If the current count equals zero then nothing happens. */ /** * 递减锁存器的计数,释放所有等待的线程,如果 *计数达到零。 *<p>如果当前计数大于零,则递减。 *如果新计数为零,则重新启用所有等待线程 *线程调度目的。 *<p>如果当前计数为零,则什么也不会发生。 */ latch.countDown();// System.out.println("子线程:" + Thread.currentThread().getName() + "执行" + latch.getCount()); } }); es1.shutdown(); //第二个子线程执行 ExecutorService es2 = Executors.newSingleThreadExecutor(); es2.execute(new Runnable() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程:" + Thread.currentThread().getName() + "执行" + latch.getCount()); latch.countDown(); System.out.println("子线程:" + Thread.currentThread().getName() + "执行" + latch.getCount()); } }); es2.shutdown(); System.out.println("等待两个线程执行完毕…… ……"); try { /** * Causes the current thread to wait until the latch has counted down to * zero, unless the thread is {@linkplain Thread#interrupt interrupted}. * * <p>If the current count is zero then this method returns immediately. * * <p>If the current count is greater than zero then the current * thread becomes disabled for thread scheduling purposes and lies * dormant until one of two things happen: * <ul> * <li>The count reaches zero due to invocations of the * {@link #countDown} method; or * <li>Some other thread {@linkplain Thread#interrupt interrupts} * the current thread. * </ul> * * <p>If the current thread: * <ul> * <li>has its interrupted status set on entry to this method; or * <li>is {@linkplain Thread#interrupt interrupted} while waiting, * </ul> * then {@link InterruptedException} is thrown and the current thread's * interrupted status is cleared. * * @throws InterruptedException if the current thread is interrupted * while waiting */ /** * 使当前线程等待直到闩锁倒计时 * 零,除非线程是 {@linkplain Thread#interrupt interrupted}。 * * <p>如果当前计数为零,则此方法立即返回。 * * <p>如果当前计数大于零,则当前计数 * 线程因线程调度目的而被禁用并且谎言 * 休眠,直到发生以下两件事之一: * <ul> * <li>由于调用了 * {@link #countDown} 方法;或者 * <li>其他一些线程{@linkplain Thread#interrupt interrupts} * 当前线程。 * </ul> * * <p>如果当前线程: * <ul> * <li>在进入这个方法时设置了中断状态;或者 * <li>在等待时{@linkplain Thread#interrupt interrupted}, * </ul> * 然后抛出 {@link InterruptedException} 并且当前线程的 * 中断状态被清除。 * * @throws InterruptedException 如果当前线程被中断 * 等待时 */ latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("两个子线程都执行完毕,继续执行主线程"); } }
打印结果:
主线程开始执行…… ……
等待两个线程执行完毕…… ……
子线程:pool-1-thread-1执行2
子线程:pool-1-thread-1执行1
子线程:pool-2-thread-1执行1
子线程:pool-2-thread-1执行0
两个子线程都执行完毕,继续执行主线程
下面我们再来看一下 state 在 ReentrantLock 中是什么含义,在 ReentrantLock 中它表示的是锁的占有情况。最开始是 0,表示没有任何线程占有锁;如果 state 变成 1,则就代表这个锁已经被某一个线程所持有了。
那为什么还会变成 2、3、4 呢?为什么会往上加呢?因为 ReentrantLock 是可重入的,同一个线程可以再次拥有这把锁就叫重入。如果这个锁被同一个线程多次获取,那么 state 就会逐渐的往上加,state 的值表示重入的次数。在释放的时候也是逐步递减,比如一开始是 4,释放一次就变成了 3,再释放一次变成了 2,这样进行的减操作,即便是减到 2 或者 1 了,都不代表这个锁是没有任何线程持有,只有当它减到 0 的时候,此时恢复到最开始的状态了,则代表现在没有任何线程持有这个锁了。所以,state 等于 0 表示锁不被任何线程所占有,代表这个锁当前是处于释放状态的,其他线程此时就可以来尝试获取了。
总结:ReentrantLock中的state表示是否有锁占用,
等于0表示无锁
大于0等于1表示有锁占用
大于1表示同一个线程多次获取,那么 state 就会逐渐的往上加。因为ReentrantLock是可重入锁,所以也有大于1的情况。
写到这里我有个疑问?
state怎么保证在多线程的原子性,以及怎么保证多线程相互通信?
第一:volatile来保证多线程之间的可见行,而且注意的是volatile在复合操作不能保证原子性,但是对于基本类型的赋值是可以保证原子性的
第二:在ReetranLock中,操作state的方法有两种:
代码如下:
volatile对于基本类型的赋值是可以保证原子性的
/** * Sets the value of synchronization state. * This operation has memory semantics of a {@code volatile} write. * @param newState the new state value */ protected final void setState(int newState) { state = newState; }
/** * Atomically sets synchronization state to the given updated * value if the current state value equals the expected value. * This operation has memory semantics of a {@code volatile} read * and write. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that the actual * value was not equal to the expected value. */ protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
unsafe.compareAndSwapInt(this, stateOffset, expect, update),这个方法我们已经非常熟悉了,它利用了 Unsafe 里面的 CAS 操作,利用 CPU 指令的原子性保证了这个操作的原子性,与之前介绍过的原子类去保证线程安全的原理是一致的。
总结:我们对 state 进行总结,在 AQS 中有 state 这样的一个属性,是被 volatile 修饰的,会被并发修改,它代表当前工具类的某种状态,在不同的类中代表不同的含义。
2、FIFO 队列
FIFO 队列,即先进先出队列,这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“排队管理器”。
当多个线程去竞争同一把锁的时候,就需要用排队机制把那些没能拿到锁的线程串在一起;而当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚刚释放的那把锁。所以 AQS 就一直在维护这个队列,并把等待的线程都放到队列里面。
这个队列内部是双向链表的形式,其数据结构看似简单,但是要想维护成一个线程安全的双向队列却非常复杂,因为要考虑很多的多线程并发问题。我们来看一下 AQS 作者 Doug Lea 给出的关于这个队列的一个图示:
在队列中,分别用 head 和 tail 来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为“当前持有锁的线程”,而在头节点之后的线程就被阻塞了,它们会等待被唤醒,唤醒也是由 AQS 负责操作的
3、获取/释放方法
获取/释放方法这些方法是协作工具类的逻辑的具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。
获取方法
我们首先来看一下获取方法。获取操作通常会依赖 state 变量的值,根据 state 值不同,协作工具类也会有不同的逻辑,并且在获取的时候也经常会阻塞,下面就让我们来看几个具体的例子。
比如 ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。
再比如,Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。
再举个例子,CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。
我们总结一下,“获取方法”在不同的类中代表不同的含义,但往往和 state 值相关,也经常会让线程进入阻塞状态,这也同样证明了 state 状态在 AQS 类中的重要地位。
释放方法
释放方法是站在获取方法的对立面的,通常和刚才的获取方法配合使用。我们刚才讲的获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。
比如在 Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;而在 CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。所以也可以看出,在不同的实现类里面,他们对于 state 的操作是截然不同的,需要由每一个协作类根据自己的逻辑去具体实现。
总结
本课时我们介绍了 AQS 最重要的三个部分。第一个是 state,它是一个数值,在不同的类中表示不同的含义,往往代表一种状态;第二个是一个队列,该队列用来存放线程;第三个是“获取/释放”的相关方法,需要利用 AQS 的工具类根据自己的逻辑去实现。
7、有什么问题,如何解决?