1 介绍
java中,锁经常是我们去使用的一个技术,无论是使用jdk原生的synchronized,或者是使用jdk提供的无锁机制,对于java.util.concurrent 的认识,都会有或多或少的理解。
一提到高性能的jdk包,就不得不提Doug Lea 这位超级大佬。
如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。
上面的这句话摘自百度百度对于Doug Lea这位老大爷的介绍,我觉得无论任何人,在去学习jdk的无锁实现的时候,都应该去搜索一下这位老大爷,是真的牛皮的啊!!
本文主要分析的是无锁机制AQS的实现,以及ReentrantLock中使用AQS实现的公平锁的源码。
2 开头
在java.util.concurrent 源码包中,AbstractQueuedSynchronizer是它的基石(这个类我们也是常叫它AQS),一切所有的无锁实现,包含ReentrantLock,CountDownLatch, Semaphore 等类,都是基于AbstractQueuedSynchronizer实现的。
首先,AQS是一个抽象类,该类继承至AbstractOwnableSynchronizer并标记为可序列化的抽象类
class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable
对于AbstractOwnableSynchronizer ,这个类也常叫它AOS,其内部持有一个Thread类型的成员变量:exclusiveOwnerThread,并包含了设置和获取该成员变量的set/get 方法
exclusiveOwnerThread成员变量的含义是:持有该同步器(也就是当前AOS对象)的线程。
对于AQS(AbstractQueuedSynchronizer) 类而言,其内部是持有一个内部的静态类Node(这个类及其之重要),并含有当前AQS所持有的head,和tail,以及 int 类型的 state 变量(实际上他们都是volatile 类型)
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
这里加一句,对于无锁而言:实际上内部实现就是通过:volatile + cas(原子操作) + 自旋 进行实现的。(这个后面的代码中,我们也会在源码中分析)
AQS中的静态类Node:官方文档给他的解释为:等待队列中的节点类
其内部主要含有以下几个成员变量:
/** Marker to indicate a node is waiting in shared mode */
// 共享模式
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
// 独占模式
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
// 代表线程取消竞争这个锁
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
// 其表示当前node的后继节点对应的线程需要被唤醒
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
// 等待状态值以指示线程在条件下等待
// 对应的条件
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
// 该表的值为上面的几种情况,不同的情况下有对应不同的结果
volatile int waitStatus;
// 当前节点的前驱节点
volatile Node prev;
// 当前节点对应的后驱节点
volatile Node next;
// 当前node所持有的线程对象
volatile Thread thread;
总之,对于AQS的源码理解,你要首先从大概的知道其内部的这些成员变量,有个印象。
3 机制
在正式分析之前,先贴出以下几个结论,
这里有一些专用名词需要去解释一下(实际上你去看AbstractQueuedSynchronizer#Node类的api文档的时候介绍的很详细)
CLH:是AQS内部关于等待队列(官方文档给的是:wait queue)的实现策略,是基于CLH锁的一种变体实现。(CLH是三个发明人的名词解析,具体实现可以搜索)
等待队列:(wait queue)本文一下都是会通过等待队列这个专有名词去分析,实际上AQS的内部实现:就是以双向链表去存储当前需要竞争锁的节点(Node)所组成的等待队列,并通过LockSupport.park unpark 来暂停和唤醒指定Node节点持有的线程来实现无锁机制
4 分析
(上面的一些结论我相信你去分析完了AQS再去回头看会更加明白。)
铺垫了半天,终于要进入正题,,再说明:本文首先我们要分析的是ReentrantLock的公平锁实现
重点, 重点, 重点:大部分的源码讲解,都贴到代码的注释中,实际上对于AQS的理解,我们的大脑中要带着多线程的思维去分析各种场景,这样,你才能理解的更透
// 内部是是通过Sync进行控制,分为公平锁和非公平锁 NonfairSync FairSync
private static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
for (int i =0 ;i < 10;i++) {
Thread thread = new Thread(() -> {
lock.lock();
try{
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread.start();
}
}
首先ReentrantLock源码中对于AQS的实现是通过内部类Sync 来实现,其内部含有两个Sync的实现类:FairSync(公平锁)和NonfairSync(非公平锁),对于公平锁的构造,只需要再构造ReentrantLock 实例的时候,构造函数传true即使用公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 加锁操作
final void lock() {
acquire(1);
}
// 该实现来自于AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 这玩意我没看懂。。。。 有啥用
selfInterrupt();
}
首先分析加锁操作,公平锁下,首先是会进入到tryAcquire(args)中
protected final boolean tryAcquire(int acquires) {
// 获取当前的线程
final Thread current = Thread.currentThread();
int c = getState();
// 当前ReentrantLock 对应的state为0 说明没有任何线程持有这个锁
if (c == 0) {
// hasQueuedPredecessors 对应公平锁而言,当然是要看对应链表队列是否还有线程等待这个锁,毕竟是公平的,有人已经等了那你肯定靠边
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 获取了锁,设置同步器的独占线程值 AOS
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 可重入锁 ,这个也说明ReentrantLock 多lock() 就需要多unlock()
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 分析一下这一步跳转的情况
// 1, c != 0 这种情况很明显,当前锁已经被指定线程所持有
// 2, hasQueuedPredecessors 返回了true,说明等待队列中已经有线程在等待竞锁
// 3, compareAndSetState(0, acquires) 执行一次cas失败,说明在同一个时间片段内,刚好有一个线程修改了state的值
// 4, c != 0 current != getExclusiveOwnerThread 有其他线程竞争了锁,很明显你不能去获取
return false;
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
// 检测当前AQS的等待队列中是否已存在指定的节点要去获锁
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
假如一个线程尝试获取一下锁tryAcquire(arg) 失败了,首先进入到addWaiter(Node.EXCLUSIVE) 方法
private Node addWaiter(Node mode) {
// 将当前线程封装成Node加入到CLH的tail
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 这里,我们会看到这两种情况
// 1,当前CLH锁等待队列链表为空(没有tail 肯定是空),
// 2,执行一次cas compareAndSetTail失败了,也就是说,与此同时(就在同一时刻) 另一个线程抢先一步把当前CLH的锁等待队列的tail设置成了它自己,
// 对于第二种情况自然不用担心,因为enp(node) 方法通过自锁操作不断cas把自己封装的Node 加入到了CLH锁等待队列链表中设置成了tail
// 对于第一种情况,enq(node) 方法通过自锁首先设置一个虚拟节点head,然后再通过自旋不断cas把自己封装的Node 加入到了CLH锁等待队列链表中设置成了tail
enq(node);
return node;
}
private Node enq(final Node node) {
// 自旋操作
for (;;) {
Node t = tail;
// 锁等待队列为空的情况下,首选需要初始化一个虚拟的节点head
if (t == null) { // Must initialize
// 其实分析cas的时候,我们要带着多线程的思维进行分析,假如在这个时候,两个线程同时对head进行赋值操作的话,会怎么样?
// 结果很明显,必然是有一个线程抢先完成了head的赋值,而另一个线程cas执行失败,失败的情况下,继续执行for操作,进行compareAndSetTail操作
if (compareAndSetHead(new Node()))
// 这个结果必然是及其短暂的(或者像是一个过度)
tail = head;
} else {
node.prev = t;
// 不必多言
// 因此 对应一个node而言,实际上总是会放到tail节点上
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
成功追加到了CLH的锁等待队列的链表的tail之后,执行acquireQueued(Node node) 方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 返回这个node 的prev
final Node p = node.predecessor();
// p == head的情况下是只能有一个,有一个线程获取到了锁,而你没有,并且当前线程初始化了CLH锁等待队列即:new Node(Head) -> node(tail)
// 显然p == head 的情况下,我是能够再去尝试拿一下锁的(CLH都没有人等锁了,我当然要去看一下拿锁的那个人有没有把锁释放了,我去拿)
if (p == head && tryAcquire(arg)) {
// 这种情况是:那个拿锁的哥们神奇的是在我tryAcquire 的时候刚好把锁给释放了
// 这种情况下太秒了,那就把我的节点node设置成当前的head,然后原始的head 的next置null ,等待gc
setHead(node); // node -1 thread
p.next = null; // help GC
failed = false;
// 这种我即没有失败,也没有被LockSupport.park()
return interrupted;
}
// 有以下两种情况导致执行这个方法
// 1,tryAcquire(arg) 没拿到锁,即别人没有锁释放掉
// p != head,这种情况下说明CLH锁等待队列里有人等了很久了,公平起见,你是没有资格去试一下锁的
if (shouldParkAfterFailedAcquire(p, node) &&
// 挂起当前线程 ,
// 还是要带着分析的角度来看这个执行的条件, 首先shouldParkAfterFailedAcquire(p, node) 为true
// 即标识当前节点的前驱节点(prev) 的waitStatus 为 -1,我需要挂起你
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 这个????? 内部自旋报异常抛出?
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 这个的意思是当前节点node 的prev节点为SINGLA,当前节点需要被挂起
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
// 这个是关键,标识当当前线程的waitStatus被设置为了CANCLED,标识取消了进程锁之后,实际上
// 如果一个节点置为waitStatus = CANCELLED,其对应的子节点(next节点)对应的prev节点会重新向前链接到一个不为CANCELLED 节点上去
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 毫无意思,初始情况下:其prev 的waitStatus == 0 对应等待线程池等待很久的线程,是会把其对应的前驱节点设置 Node.SIGNAL
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上面的shouldParkAfterFailedAcquire 逻辑需要你去慢慢理解,实际上,对于一个执行了这一步的的操作,主要有以下两种情况
1,如果当前节点的node 前驱节点为head的时候,很显然,此时有一个线程持有了锁,但是它在当前线程去再试一次的时候没有释放,此时当前节点的前驱节点(pred.waitStauts) 必然是0(都没人设置它呀),所以,会进入到compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 将当前节点的前驱节点设置为 SINGNAL, 然后再自旋,再获锁(万一又没获取到),返回true
2, 如果当前节点的node前驱节点pred不是head的时候,很显然,公平起见,pred必然是会优先你去获锁的。并且如果他的前驱节点pred的waitStauts为CANCELLED 的话,其对应的子节点(next节点)对应的prev会重新向前链接到一个不为CANCELLED 节点上去,然后再自旋的将新链接的前驱节点设置SIGNAL,然后再自旋一次返回true。
因此,当当前节点的前驱节点的waitStatus为SIGNAL,意味着当前节点需要挂起
shouldParkAfterFailedAcquire返回ture之后,执行到了parkAndCheckInterrupt 方法
private final boolean parkAndCheckInterrupt() {
// 挂起了当前的线程,这个线程在这里阻塞了
LockSupport.park(this);
// 获取当前线程是否被中断,这个应该由谁来出现处理这个线程的thread.interceptor 呢?
return Thread.interrupted();
}
然后分析解锁操作:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 解锁操作
if (tryRelease(arg)) {
Node h = head;
// 这个写法很牛皮呀
// 很显然,只要是处于CLH锁等待队列种的Node,如果他的waitStauts不为初始值0 的情况下,都在等锁,你调用unlock也没有意义 的啊
if (h != null && h.waitStatus != 0)
//
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
// 重新将head的node waitStatus 设置为0
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
// 递归获取当前head节点
// head 得下一个节点,判断它是不是要挂起
// 如果下一个是null得情况下,这个还真有可能,这个说明CLH得锁等待队列都拿到锁了
if (s == null || s.waitStatus > 0) {
s = null;
// 这个代码写的很牛皮,s.waitStatus > 0 说明 CANCELLED
// 从尾部向前递归
// 这种情况下是不可能由s == null 触发得
// 由s.waitStatus > 0 触发,这个条件说明node.next 已经拿到锁了
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒后驱节点的线程
LockSupport.unpark(s.thread);
}
5 带着多线程的头脑去分析
下面是一个多线程的获锁场景,我们可以尝试分析。更加加深AQS的获锁逻辑
/**
* 这样分析 1,假设一个线程无限长的lock持有了这个锁
* 2,此使一个线程A试图lock这个锁,此使同步器中的CLH的锁等待链路实际上是
* new Node(waitStatus = 0) -> Node(Current Thread a, Node.EXCLUSIVE[NULL], waitStatus = 0)
* 那么,我们分析此使线程A是会停落再哪呢?
* #parkAndCheckInterrupt() 这个方法中 内部调用了LockSupport.park(this)
* 3,此使另一个线程B也试图lock这个锁(先不考虑线程几乎同时的情况),此时同步器中的CLH的锁等待链路实际上是:
* new Node(waitStatus = -1) -> Node(Current Thread A, Node.EXCLUSIVE[NULL], waitStatus = 0) -> Node(Current Thread B ,Node.EXCLUSIVE[NULL], waitStatus = 0)
* 那么,我们再分析此时线程B会停落再哪呢?
* #parkAndCheckInterrupt() 这个方法中 内部调用了LockSupport.park(this),毫无疑问,但此时,同步器中的CLH的锁等待链路变成了
* new Node(waitStatus = -1) -> Node(Current Thread A, Node.EXCLUSIVE[NULL], waitStatus = -1) -> Node(Current Thread B ,Node.EXCLUSIVE[NULL], waitStatus = 0)
*
*/
6 非公平锁
故名思意,非公平锁就是每一个线程进来获锁的时候,我都要尝试一下进行cas操作去判断我是否获取成功,如果成功了,更好,不成功了之后,我再去加入到CLH的锁等待队列里,然后锁被释放了之后,唤醒线程之后,我再去cas判断一下是否获锁成功过(注意此时是不会判断锁等待队列中是否有已经再等待的节点,直接去cas)。
// 1,非公平锁很明显,每一次获取之前都要去竞争一下锁,如果这个时候刚好竞争到锁了之后,那就直接拿锁
// 2,如果没有拿到锁之后,和公平锁一致,调用acquire(1)方法,但是会进入到非公平锁的tryAcquire() 方法,再试一下
// 对于非公平锁而言,它不会去看CLH锁等待队列种是否有节点等待,而是直接再去cas获取锁,
// 实际上对于公平锁和非公平锁的区别,就是这两点,一个是lock() 方法的区别 另一个是tryAcquire() 方法的区别
// 相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
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;
}