前言
为什么需要去了解AQS,AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件
本文所有源码基于JDK9
目的:掌握大概的流程/框架
适用人群:初学者想了解些源码的
不适合:想深入了解的
ReentrantLock-非公平锁
我们在实际中一定会用到ReentrantLock的lock操作,那么它的实现究竟是怎样的?我们以重入锁作为切入点。
1、构建锁,获得锁对象
//锁的声明
private final Sync sync;
// 构造锁,默认非公平
public ReentrantLock() {
sync = new NonfairSync();
}
2、lock方法,调用sync这个锁对象
public void lock() {
sync.acquire(1);
}
// 来自AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
关键性的四个方法:
1、tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
2、addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
3、acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
4、selfInterrupt:产生一个中断。
tryAcquire
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
加锁,是通过NonfairSync的这个方法实现的,但是NofairSync并没有它的实际代码。真正实现的是它的父类Sync。
nonfairTryAcquire(int acquires)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
getState(),这个方法在AQS中,用户获取锁的状态值。
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
下面来解释一下state在独占锁的作用是什么,结合下nonfairTryAcquire(int acquires)。
- 首先第一个if条件,state = 0 的时候代表着,当前锁没有被某个线程占用,然后通过CAS操作设置线程的状态为值1,并且把当前线程设置为独占锁的拥有者。
- 第二个if,当state!=0的时候,代表这个锁已经被别的线程占着了,就判断,这个是不是这个锁的拥有者,如果是的话,锁state就+1,这就巧妙地设置了重入锁的。每次+1的机制
CLH同步队列
在介绍addWaiter前,先来看一下CLH同步队列,就看着大佬们在说CLH但是少有人说他是啥,我解释一下啊
The wait queue is a variant of a “CLH” (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks
1、名称:CLH 由三个人名字组成 (Craig, Landin, and Hagersten)
2、基本数据结构:基于FIFO双端链表
3、用途:用于等待资源释放的队列。也就是等待锁释放的队列
注意:由于笔者是初学者,觉得CLH队列中的等待状态转换略微复杂,故意跳过。只看它们的方法,并没有特别深入
addWaiter
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
与JDK8不同的地方是,addWaiter直接使用自旋(无限循环)去完成入队的操作,而不是调用enq,实现的内容和enq差不多。下面给出enq的代码
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(Node node) {
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return oldTail;
}
} else {
initializeSyncQueue();
}
}
}
注意addWaiter的返回值,是新加入的节点下面有用
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
try {
// 中断标识
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor();
// 如果当前线程节点的前驱节点是头结点,并且尝试获得锁成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
// 如果获取锁失败了,就进入挂起。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
关于挂起和唤醒,就先不看了,主要还是理解下流程。过多的细节会拖慢新手的学习之路,一定要记住这个!!
RenentrantLock-公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁和非公平锁在于第一个判断条件中的,hasQueuedPredecessors()这个方法。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 头节点不是尾节点
// 第一个节点不为空
// 当前节点是头节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
从源码我们可以验证,公平和非公平的标准是否是按照队列的顺序进行锁的获取的原理。
小结:
到这里,加锁基本上就是结束了,我们忽略了挂起和唤醒这种复杂的操作。加锁的操作中,阻塞队列是由AQS使用CLH进行维护的。ReentrantLock,的同步操作主要还是依赖于AQS。
释放锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
调用Sync的tryRelease,减小state值,然后uppark进行唤醒下一个节点PS,笔者忽略了唤醒的操作。
总结
本文针对和我一样刚入门不久的新手,从整体上以重入锁的加锁和解锁,去了解了一些AQS的CLH队列的基本内容,未涉及深层次,顺便看了下公平和非公平的实现。算作是一种了解把,个人感觉太细节的东西,新手看了也没用。还是从宏观上把握把握,会用,然后了解点源码,方便以后再来学习。