天天看点

并发编程之:AQS源码解析

大家好,我是小黑,一个在互联网苟且偷生的农民工。

在Java并发编程中,经常会用到锁,除了Synchronized这个JDK关键字以外,还有Lock接口下面的各种锁实现,如重入锁ReentrantLock,还有读写锁ReadWriteLock等,他们在实现锁的过程中都是依赖与AQS来完成核心的加解锁逻辑的。那么AQS具体是什么呢?

提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。 该类被设计为大多数类型的同步器的有用依据,这些同步器依赖于单个原子int值来表示状态。 子类必须定义改变此状态的受保护方法,以及根据该对象被获取或释放来定义该状态的含义。 给定这些,这个类中的其他方法执行所有排队和阻塞机制。 子类可以保持其他状态字段,但只以原子方式更新int使用方法操纵值getState() , setState(int)和compareAndSetState(int, int)被跟踪相对于同步。

上述内容来自JDK官方文档。

简单来说,AQS是一个先进先出(FIFO)的等待队列,主要用在一些线程同步场景,需要通过一个int类型的值来表示同步状态。提供了排队和阻塞机制。

并发编程之:AQS源码解析

从类图可以看出,在ReentrantLock中定义了AQS的子类Sync,可以通过Sync实现对于可重入锁的加锁,解锁。

AQS通过int类型的状态state来表示同步状态。

AQS中主要提供的方法:

acquire(int) 独占方式获取锁

acquireShared(int) 共享方式获取锁

release(int) 独占方式释放锁

releaseShared(int) 共享方式释放锁

独占锁和共享锁

关于独占锁和共享锁先给大家普及一下这个概念。

独占锁指该锁只能同时被一个线程持有;

共享锁指该锁可以被多个线程同时持有。

举个生活中的例子,比如我们使用打车软件打车,独占锁就好比我们打快车或者专车,一辆车只能让一个客户打到,不能两个客户同时打到一辆车;共享锁就好比打拼车,可以有多个客户一起打到同一辆车。

我们简单通过一张图先来了解下AQS的内部结构。其实就是有一个队列,这个队列的头结点head代表当前正在持有锁的线程,后续的其他节点代表当前正在等待的线程。

并发编程之:AQS源码解析

接下来我们通过源码来看看AQS的加锁和解锁过程。先来看看独占锁是如何进行加解锁的。

可以看到在ReentrantLock的lock方法中,直接调用了sync这个AQS子类的lock方法。

在获取锁时,基本可以分为3步:

尝试获取,如果成功则返回,如果失败,执行下一步;

将当前线程放入等待队列尾部;

标记前面等待的线程执行完之后唤醒当前线程。

在整个加锁过程可以通过下图更清晰的理解。

并发编程之:AQS源码解析

同样解锁时也是直接调用AQS子类sync的release方法。

解锁过程如下:

先尝试解锁,解锁失败则直接返回false。(理论上不会解锁失败,因为正在执行解锁的线程一定是持有锁的线程)

解锁成功之后,如果有head节点并且状态不是0,代表有线程被阻塞等待,则唤醒下一个等待的线程。

为了实现共享锁,AQS中专门有一套和排他锁不同的实现,我们来看一下源码具体是怎么做的。

tryAcquireShared尝试获取共享许可,本方法需要在子类中进行实现。不同的实现类实现方式不一样。

下面的代码是ReentrentReadWriteLock中的实现。

本方法可以总结为三步:

如果有写线程独占,则失败,返回-1

没有写线程或者当前线程就是写线程重入,则判断是否读线程阻塞,如果不用阻塞则CAS将已使用读锁个数+1

如果第2步失败,失败原因可能是读线程应该阻塞,或者读锁达到上限,或者CAS失败,则调用fullTryAcquireShared方法。

AQS是很多并发场景下同步控制的基石,其中的实现相对要复杂很多,还需要多看多琢磨才能完全理解。本文也是和大家做一个初探,给大家展示了核心的代码逻辑,希望能有所帮助。

好的,本期内容就到这里,我们下期见;关注公众号【小黑说Java】更多干货。

并发编程之:AQS源码解析

继续阅读