Java的内置锁一直备受争议,在JDK1.6之前,synchronized这个重量级锁其性能一直较为低下:虽然在java1.6开始,进行了大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷的隐式获取锁和释放锁的机制(基于JVM机制),但是它却缺少了获取锁和释放锁的可操作性,可中断超时获取锁,且它在独占式的高并发环境下性能大打折扣:
常见概念:
自旋:什么是自旋,自旋就是一个通过循环不断尝试的过程。
AQS队列:结构是使用双向链表设计的
ReentrantLock源码分析之上锁:🔒
AQS(AbstractQueuedSynchronizer)类:具体部分看源码!
private transient volatile Node head; // 队首
private transient volatile Node tail; // 队尾
private volatile int state; // 锁状态 0表示未上锁,加锁成功则 + 1
AQS队列展示:
如上图可以看出:
- AQS队列中每一个元素都是一个Node,其中Node节点包括了Thread、prev和next几个属性。其中Thread代表哪一个线程、prev代表前驱节点、next代表后置节点。
- 从图中可以看出AQS队列的第一个Node Thread为Null,后面介绍为什么为NUll。
- 从图中可以看出AQS是使用的双向链表,这个在阅读源码的时候会知道原因,在源码中AQS新入队一个Node需要修改AQS队列中前一个Node的ws从0变成-1,同时在唤醒新的Node时又需要唤醒下一个节点。所以我认为这里使用双向链表是为了更加方便,提升性能。
- 其中head永远指向队列的head也就是Thread为null的位置,tail指向队列的尾部。
Node类设计:
static final class Node{
volatile int waitStatus; // ws状态 0表示未休眠,-1表示已经休眠
volatile Thread thread; // 当前线程
volatile Node prev; // 前驱指针
volatile Node next; // 后置指针
}
上锁的过程:🔒
锁对象:这里的锁对象就是指ReentrantLock的实例化对象。
未加锁状态:未加锁状态就是没有线程持有该对象,计数器为0;
计数器:在Lock对象中存在state属性,用来记录上锁的次数,比如未上锁,则state的值为0,大于0则表示有其他线程持有该锁,小于0则会抛出异常。
waitState:就是一个状态,初始值为0,可以让一个第一个如队的线程自旋两次,减少直接park损失性能。ws默认为0 是有必要的。
tail:队列的尾部;head队列的头部;t1第一个线程;t2第二个线程;t3第三个线程。
节点:上面介绍的Node就是AQS队列中的节点,里面封装了线程,所以从某种意义上说Node就是一个线程。
下面主要解释一下公平锁的源码:
acquire方法源码分析:
// 1、首先调用tryAcquire方法尝试获得锁
// 2、使用addWaiter方法,将线程加入到队列中
// 3、acquireQueued设置park阻塞
// 4 selfInterrupt 设置自我中断,修改标志位。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法源码分析:
/**
* * Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前持有锁的状态,为0则表示未上锁,大于0表示已经获得锁
int c = getState();
if (c == 0) { // 没有人占用锁---->我要占用
// hasQueuedPredecessors:判断自己是否需要排队,比较复杂
// compareAndSetState:使用CAS设置state状态为1,如果成功则把线程设置成持有锁的状态。
// 继而返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 有人占用锁,则判断占用锁的线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 如果是当前线程持有锁则state + 1,这里可以看出ReentrantLock的重入锁的实现。
int nextc = c + acquires;
// state正常情况下是不会小于0的
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 更新state值
setState(nextc);
return true;
}
return false;
}
hasQueuedPredecessors 方法源码分析:
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.
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());
}
下面对hasQueuedPredecessors方法进行分析:
一、队列没有被初始化,不需要排队;直接去加锁,但是有可能会失败;因为可能有几个线程同时获得lock,并且都发现没有初始化,于是所有线程都认为不需要要排队即可以直接获得锁,所以这里要使用CAS修改state的值;这里如果t1获得锁成功,那么t2CAS就会失败,那么t2就会去初始化队列。
二、队列被初始化了,但是t2过来加锁,发现队列中的第一个就是自己;
那么这种情况下是怎么发生的呢,首先如果上一个线程已经完成会设置当前线程为head,线程就会重新调用tryAcquire尝试重新获得锁,如果获得成功直接加锁,获取失败则重新进入队列,所以这里的不需要要排队,不是真的不需要排队。
h != null 判断head不等于tail的情形:
- 当队列没有被初始化,那么head和tail都是null,所以这里就会返回false,那么短路与就不会执行,直接返回false就是不需要排队。!hasQueuedPredecessors()使用!做取反,所以就会执行,并且尝试获得锁。
- 队列被初始化了,如果队列初始化了那么h!=t则成立。
- 如果队列被初始化了,而且至少有一个人在排队那么自己也去排队,他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放也合情合理,好比你去买票,如果有人排队,那么你乖乖排队,但是你会去看第一个排队的人是不是你女朋友;如果是你女朋友就相当于是你自己(这里实在想不出现实世界关于重入的例子,只能用男女朋友来替代);你就叫你女朋友看看售票员有没有搞完,有没有轮到你女朋友,因为你女朋友是第一个排队的.
TODO:这里理解的不是很深,等具体深入理解后更新。
addWaiter 方法源码分析:
将一个线程加入到AQS队列中,如果队列没有初始化过需要先对队列进行初始化,如果已经初始化过了则直接最队尾添加即可。
private Node addWaiter(Node mode) {
// 使用当前线程创建一个Node节点
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;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
对于上面的入队操作,分别对t1,t2,t3,三种不同时刻的线程进行介绍:
t1正在执行 t2入队图:
- Node node = new Node(Thread.currentThread(), mode);使用当前线程创建一个新的Node
- Node pred = tail;设置pred节点为tail,此时由于队列没有初始化,所以head和tail都为null
- 于是执行enq(node);方法,初始化队列。
- Node t = tail; 创建了一个Node t,
- if (t == null) 此时必须进行初始化,使用compareAndSetHead(new Node()) 利用CAS比较并设置机制将Head节点设置为一个新创建的节点,并将tail指向新的节点。
- 第二次循环将Node设置为新的Tail,并将新的Tail返回。
经过队列的初始化和第一个线程入队以后,AQS队列中如上图所示。
t2 已经在队列中,t3入队图:
- Node node = new Node(Thread.currentThread(), mode);使用当前线程创建一个新的Node
- Node pred = tail; 设置Node的前一节点为tail,连接上一节点
- if (pred != null) 此时不为null
- node.prev = pred; 设置Node的前驱节点为,上一个节点
- 如果compareAndSetTail(pred, node)成立,pred.next = node;设置上一个节点的下一个节点为当前的Node,返回Node信息。
此时AQS队列中的节点如图所示:
acquireQueued方法源码分析:
final boolean acquireQueued(final Node node, int arg) { // 这里的Node为当前线程封装的哪个Node,后面叫做nc
// 标记很重要
boolean failed = true;
try {
// 中断标记
boolean interrupted = false;
for (;;) {
// 获得nc的上一个节点,两种情况:1、上一个节点为头部;2、上一个节点不是头部
final Node p = node.predecessor();
// 如果nc的上一个节点为头部,则表示nc为AQS队列中的第二个元素,是队列中第一个排队的Node
// 如果nc为队列中的第二个元素,需要调用tryAquire获取加锁。
// 这里只有nc为队列中第二个元素的时候,才会尝试去加锁,其他情况直接park。
// 因为第一个排队的在这里的时候需要看持有锁的线程是否释放锁,如果释放直接获得,就不park了。
if (p == head && tryAcquire(arg)) {
// 能够进入到这里面,证明上一个人已经搞完了,同时我也已经获得锁了,那么前面那个人直接出队列,我自己则是队首
setHead(node);
// 这里设置搞完事情的那个人,由于事情做完了要出队,直接使用赋值Null交由JVM负责。
p.next = null; // help GC
// 设置标示为false
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
加锁过程总结:
如果第一个线程执行,或者多个线程是交替执行的,那么不会进入到队列中,也不会初始化队列,永远与AQS无关,都是线程直接持有锁;如果发生了竞争,比如t1在持有锁的过程中,t2想来获得这把锁就会产生竞争,那么这个时候就会初始化AQS队列,在初始化AQS队列的过程中会创建一个Thread为Null的Node。在AQS队列中,head节点,也就是AQS队列中的第一个节点,永远是正在持有锁的线程。当t1执行完后会主动唤醒t2,此时t2的Thread会置为NUll,同时会给执行完的哪个Node的prev和head都设置成Null,便于GC。
这里只是记录了一些自己的学习记录,研究的没有那么透彻,有问题请大家直接指出,谢谢。