整体介绍

概念


 

 

首先阅读一下类的源码注释,可以知道,这几个接口是最关键的。


 

这几个方法是使用AQS类的关键,只有这几个方法是可以定制的,其他方法几乎都是final的,不可修改。

从代码实现上看,能看到的变量几乎都是volatile的,能看到的方法几乎都是CAS或者Unsafe类的原子方法。

 

接下来我们来整理一下这个类的字段和方法,这里我们主要关注private字段和public方法。

字段

volatile int state:同步状态。

volatile Node head:等待队列的头,延迟初始化。

volatile Node tail:等待队列的尾部。初始化后,仅通过casTail修改。

 

方法

获取锁:

acquire(int arg)

acquireShared(int arg)

 

释放锁:

release(int arg)

releaseShared(int arg)

 

等待队列:

hasQueuedThreads()

getFirstQueuedThread()

isQueued(Thread thread)

getQueuedThreads()

getExclusiveQueuedThreads()

getSharedQueuedThreads()

 

条件对象ConditionObject:

await()

signal()

 

架构

AQS整体结构,包括加锁/释放锁,条件对象await/signal。


 

功能说明

数据结构

锁的数据结构

 


 

 

 

一个状态字段state表示同步状态,0表示没有锁竞争,1表示有锁竞争。

head跟tail分别是等待队列的头节点和尾节点,该等待队列是用双向链表实现的。

 

 

 

 

 

节点的数据结构


 

 

prev:前驱节点

next:后继节点

waiter:等待锁的线程

status:节点状态

 

节点 

 

节点状态其实有4种:

取消状态:status<0;

休眠状态:status=0;

等待状态:status=1;

条件等待:status=2;

 

功能实现

  • 锁类型
  • 独占锁
    • 公平锁
    • 非公平锁
  • 共享锁
    • 公平锁
    • 非公平锁

 

独占锁acquire(int arg)

以独占模式获取,忽略中断。通过调用至少一次{@link#tryAcquire}来实现,并在成功时返回。否则线程将排队,可能会重复阻塞和取消阻塞,调用{@link#tryAcquire},直到成功。此方法可用于实现方法{@link Lock#Lock}。


 

独占锁实现过程

 

 

 

 

1.tryAcquire

尝试以独占模式获取。此方法应查询对象的状态是否允许以独占模式获取对象,如果允许,则应获取对象。

执行acquire的线程总是调用此方法。如果此方法报告失败,acquire方法可能会将线程排队(如果尚未排队),直到其他线程发出释放信号。这可以用来实现方法{@link Lock#tryLock()}。

这是一个protected方法,在ReentrantLock里实现了FairSync和NonfairSync,也就是公平锁和非公平锁。

1-1.FairSync in ReentrantLock


 

 

判断队列中有没有其他线程在等待锁,或者当前线程是第一个在等待锁的线程,也就是等待队列中第一个线程,然后CAS尝试修改锁状态,设置当前线程为锁拥有者。

 

 

 

1-2.NonfairSync in ReentrantLock

 


 

不去判断当前线程是否是等待队列中的第一个线程,直接CAS尝试获取锁。

 

 

2.acquire

在tryAcquire获取锁失败后,acquire主要做的其实是将当前线程放入等待队列中,然后在循环中判断是否可以参与锁争抢。

由于等待队列优先调度第一个节点进行锁争抢,这里要根据当前线程是否是等待队列中的第一个节点分情况讨论:

当前线程在等待队列中不存在;

当前线程是等待队列中第一个节点;

当前线程不是等待队列中第一个节点;

 

 

 


 

 

如果当前节点已存在,且不是头节点,清理队列中无效节点,并且节点继续等待;

如果当前节点已存在,且是头节点,尝试再次CAS加锁;如果加锁成功,并且是共享锁,唤醒后继节点参与锁争抢;如果加锁失败,该节点进入休眠状态,设置一个短暂的休眠期,休眠期内无法被唤醒参与锁争抢;

如果当前节点不存在,创建节点,放入等待队列尾部,进行排队等待锁争抢。

如果被中断,或者在超时时间内没有获取到锁,则加锁失败,从等待队列中清除。

 

 

 

 

 

共享锁acquireShared(int arg)

在共享模式下获取,忽略中断。通过至少调用一次{@link#tryAcquireShared}来实现,并在成功时返回。否则线程将排队,可能会重复阻塞和取消阻塞,调用{@link#tryAcquireShared},直到成功。

 

共享锁跟独占锁最大的区别在于,共享锁可以设置并发数。共享锁status表示可以同时争抢锁的线程数,也就是并发数>1,独占锁并发数=1。

 

 

共享锁实现过程


 

 

 

 

1.tryAcquireShared

这是一个protected方法,在Semaphore里实现了FairSync和NonfairSync,也就是公平锁和非公平锁。

1-1.FairSync in Semaphore

 

 

如果有前驱节点在等待,返回失败;

如果剩余的并发数<0,返回失败;

 

1-2.NonfairSync in Semaphore

 


 

 

不去判断是否有前驱节点在等待,直接根据剩余的并发数来判断。

 

 

2.acquire

在独占锁中分析过,这里不再赘述。不同点在于共享锁允许多个线程获取锁。

 

其实到这里锁的基本功能已经差不多了,如果只是实现线程之间的互斥,那么只需要用到加锁/释放锁就够了。但是如果在互斥的基础上还要进行线程间的协作,就要用到条件对象了。

 

条件对象ConditionObject

数据结构

条件对象的主要数据结构就是一个条件队列,用于存在调用await方法释放锁后的线程。


 

 

功能实现

await方法说明:

将当前线程放入条件队列,然后释放锁,唤醒等待队列中的线程参与加锁。


 

 

 

 

signal方法说明:

将等待时间最长的线程(如果存在)从该条件的等待队列移动到拥有锁的等待队列。


最后,我想说的是,AQS的实现思想并不仅仅局限于在读写锁中使用,在很多Java中间件、JVM以及操作系统中都有运用,包括其他语言中也有运用。因为这是一个通用的问题,大家都会遇到并且需要解决。我们日常业务开发中也会遇到,只不过需要使用分布式版本的解决方案。