Java并发编程"锁"篇4-AbstractQueuedSynchronizer之AQS

309 阅读6分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

0. 面试题说起

  • reentrantLock 实现原理,简单说下AQS

1. 基本介绍

AQS在字面意思是抽象的队列同步器,是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态

在这里插入图片描述

通常地:AbstractQueuedSynchronizer简称为AQS

在这里插入图片描述

CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO

2. AQS为什么是JUC内容中最重要的基石?

与AQS相关的内容如下:

在这里插入图片描述

进一步理解锁和同步器的关系

  • 锁,面向锁的使用者,定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可
  • 同步器,面向锁的实现者,比如java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等

加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要队列。

抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

在这里插入图片描述

3. AQS初步

AQS官网解释如下:

在这里插入图片描述

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

在这里插入图片描述

AQS内部体系架构如下: 在这里插入图片描述 在这里插入图片描述

  • int类型的变量state,即AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态,零就是没人,自由状态可以办理,大于等于1,有人占用窗口,等着去,如下:

    /**
     * The synchronization state.
     */
    private volatile int state;
    
  • CLH队列,CLH队列(三个大牛的名字组成),为一个双向队列,类似于银行候客区的等待顾客。 在这里插入图片描述

有阻塞就需要有排队,实现排队必然需要队列,state变量+CLH双端队列

在AQS还有一个重要的内部类Node(Node类在AQS类内部),该类中有一个表示等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个 Node。如下:

    volatile int waitStatus;

Node类内部结构如下: 在这里插入图片描述

Node类中的属性说明如下图:

在这里插入图片描述

AQS同步队列的基本结构如下图所示

在这里插入图片描述

4. 从ReentrantLock开始解读AQS

Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的。

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

ReentrantLock类的架构如下:

在这里插入图片描述

从最简单的lock方法开始看看公平和非公平如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件hasQueuedPredecessors() hasQueuedPredecessors是公平锁加锁时**判断等待队列中是否存在有效节点的方法

在这里插入图片描述

从非公平锁走起,方法lock()走起,对比公平锁和非公平锁的 tryAcquire() 方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()

hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

在这里插入图片描述

本次讲解我们走非公平锁作为案例突破口,代码例子如下:

public class AQSDemo
{
    public static void main(String[] args)
    {
        ReentrantLock reentrantLock = new ReentrantLock();//非公平锁

        // A B C三个顾客,去银行办理业务,A先到,此时窗口空无一人,他优先获得办理窗口的机会,办理业务。
        // A 耗时严重,估计长期占有窗口
        new Thread(() -> {
            reentrantLock.lock();
            try
            {
                System.out.println("----come in A");
                //暂停50分钟线程
                try { TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
            }finally {
                reentrantLock.unlock();
            }
        },"A").start();

        //B是第2个,B一看到受理窗口被A占用,只能去候客区等待,进入AQS队列,等待着A办理完成,尝试去抢占受理窗口。
        new Thread(() -> {
            reentrantLock.lock();
            try
            {
                System.out.println("----come in B");
            }finally {
                reentrantLock.unlock();
            }
        },"B").start();


        //C是第3个,C一看到受理窗口被A占用,只能去候客区等待,进入AQS队列,等待着A办理完成,尝试去抢占受理窗口,前面是B顾客,FIFO
        new Thread(() -> {
            reentrantLock.lock();
            try
            {
                System.out.println("----come in C");
            }finally {
                reentrantLock.unlock();
            }
        },"C").start();

    }
}

lock()如下:

在这里插入图片描述

acquire()如下: 在这里插入图片描述 在这里插入图片描述

tryAcquire(arg),本次走非公平锁: 在这里插入图片描述 在这里插入图片描述

nonfairTryAcquire(acquires)如下: 在这里插入图片描述

addWaiter(Node.EXCLUSIVE)如下: 在这里插入图片描述

如果当前的CLH队列为空的时候就会执行enq(node)方法,否则直接将当前线程组成的Node结点加入到CLH队列的末尾,enq(node)如下: 在这里插入图片描述

注意: 双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)如下:

在这里插入图片描述

假如再抢抢失败就会进入以下三个方法判断中

在这里插入图片描述

shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中 在这里插入图片描述 shouldParkAfterFailedAcquire 如下: 在这里插入图片描述

如果前驱节点的 waitStatus 是 SIGNAL状态,shouldParkAfterFailedAcquire 方法会返回 true 程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起

parkAndCheckInterrupt 如下:

在这里插入图片描述

ReentrantLockunlock方法主要底层是通过LockSupport.unpark(s.thread);唤醒之前加入CLH被阻塞的线程。

sync.release(1)->tryRelease(arg)->unparkSuccessor

参考资料

Java并发编程知识体系
Java并发编程的艺术
Java多线程编程核心技术
Java并发实现原理 JDK源码剖析