AQS

1、为什么学AQS?

理解其背后的原理、学习设计思想,以提高技术并应对面试

1.1、AQS 的重要性

我们先来介绍一下 AQS(AbstractQueuedSynchronizer)的重要性,来看看 AQS 被用在了哪些类里面。

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的时候,唤醒主线程继续执行

 案例一:

AQS_工具类_02AQS_阻塞状态_03
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("两个子线程都执行完毕,继续执行主线程");
    }
}
View Code

 

 打印结果:

主线程开始执行…… ……
等待两个线程执行完毕…… ……
子线程: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 给出的关于这个队列的一个图示:

AQS_工具类_04

 

 在队列中,分别用 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、有什么问题,如何解决?