ReentrantLock详解
1.核心部分
AbstractQueuedSynchronizer 核心操作类(不仅仅是在Lock领域,线程池也多有应用,俗称AQS)
Sync 继承AQS,提供了对Lock的部分操作
NonfairSync 继承Sync,见名知意,非公平锁的操作(默认为非公平所)
FairSync 继承Sync,见名知意,公平锁的操作
2.获取锁的过程
这里默认使用非公平锁(公平锁逻辑上类似。细节有所不同)
===========================================
方式1:lock();
方式2:tryLock();
方式3:tryLock(long timeout, TimeUnit unit);
===========================================
方式1:lock();
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
个人认为比较好理解,首先进行一个原子操作compareAndSetState(0, 1),lock的state在初始状态或者未被占有的状态下为0,这里是一种基于乐观锁的设计。
若是成功设置lock的state,设置当前线程为lock的owner。
否者进入acquire(1);
acquire(1);
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
官方释义:以独占模式来获取锁,且忽略中断。若成功获取锁,返回。否者有可能进入队列等待。
if判断条件分为2个,分别解析。
(1)tryAcquire(arg)
定义于AQS内,实现在ReentrantLock的NonfairLock中
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(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;
}
首先获取当前线程,判断当前lock的state(万一在我们第一次获取锁失败的时候刚好有人释放了呢),若lock的state是0,则尝试再去取compareAndSetState(0, acquires),成功返回true,否则返回false。
若是state!=0,两种情况。1.该线程本身就是lock的持有者,由于ReentrantLock支持重入,所以在符合最大重入次数的条件下,设置state++,返回true。
2.该线程不是lock的持有者,那么相当于又一次尝试获取锁失败。返回false.
tips:我认为这里能体现部分作为“非公平所”的特性,就是说,一个线程在第一次获取锁失败的情况下,还有第二次获取的机会,假设他与一个在等待队列中等待了许久的线程竞争,结果他回去了锁,是不是就意味着不公平?
再看acquire(int arg)的if语句中的2个条件,如果第一个tryAcquire返回的是true,根据短路与的特性,后面的都不执行,线程成功的获取lock,返回。
若是返回的是false,我们继续看接下来的条件
(2)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
先看addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
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)) {//设置新的tail结点
pred.next = node; //由于是双向链表,所以要设置prev以及next
return node;
}
}
enq(node);
return node;
}
首先,根据函数名就能猜到,该操作的结果是产生一个等待着,也就是使当前线程加入等待队列。
这里介绍下内部等待队列的数据结构。
注意:是一个双向链表,这里并没有标出。
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
Node node = new Node(Thread.currentThread(), mode);可以看到根据传入的Node的mode来创建出一个node,该node的内部thread指向当前线程。
tail指向尾节点,head指向头结点,可以认为,在第一个线程进入等待队列时,tail以及head都是空的。
所以先来关注enq(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;
}
}
}
}
我们来看,第一个将要等待队列的线程到来时,t必然为null。所以第一次会触发compareAndSetHead(new Node()),结果也就是产生一个辅助节点作为head节点。
然后我们需要关注的是这是一个for(;;),也就是死循环,第二次循环时,由于head节点已经被创建,那么node就会被插入到链表尾部,也就是prev指向前一个节点,tail指向该node。
说白了就是若等待队列为空,构造出一个等待队列(辅助的head结点,tail也指向head),再将传入的node插入队列。移动tail指针。
回过头去看addWaiter(Node.EXCLUSIVE)
其实做的只是插入一个结点,并且移动tail指向新插入的结点。
再来看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); //获取前一个结点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
要理解 if (p == head && tryAcquire(arg)),由于p是传入结点的前一个结点,如果p是head,则再次尝试去获取锁,如果获取到了,开始setHead(node)操作。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead(node);做的只是将head指向该node,同时置空一些node的信息,因为他已经获取到锁了,所以不需要再等待队列中了。
由此可见,在等待队列中最有可能能回去lock的是head后的第一个结点。
若是if (p == head && tryAcquire(arg))不满足,则进入 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) 分支。
shouldParkAfterFailedAcquire(p, node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) { //大于0 表示已经被cancel掉了 也就是无效的了
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这段代码不是很直面。
首先waitStatus的含义是指该结点内thread的等待状态,由以下几种组成:
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1; //表示该线程已取消
static final int SIGNAL = -1; //标示该线程需要被挂起
static final int CONDITION = -2; //标示该线程正在等待信号量
static final int PROPAGATE = -3; //等待共享锁
if (ws > 0) 若是满足此条件,从该结点开始,向前扫描,找到第一个状态不为cancel的结点。然后与找到的结点连接(next,prev),若是下一个结点的状态小于0,则尝试将其置为SIGNAL。
在关注外部的for(;;)是一个死循环,也就是说,第二次来遍历的时候必然会发现状态为SIGNAL的结点(不为cancel)。找到后返回true。
parkAndCheckInterrupt()
该操作只是将当前线程挂起到WAITING状态,等待一个中断或者unpark()方法来唤醒他。
若是if操作成功,则将Interrupt的值设置为true,表示需要中断来唤醒。
再回到顶层
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
假设此时tryAcquire(arg);没有获取到锁,return false; acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 返回false 说明等待队列的head之后的第一个结点已获取锁,且Interrupt的值为false,代表不需要中断操作来唤醒。若返回true,则说明队列中有一个结点已经获取锁,但是需要唤醒当前线程。
所以selfInterrupt();该操作的目的就是产生一个中断,是线程从WAITING中恢复。
具象一点的说法,假设现有N个线程未获取lock进入到了等待队列,每个线程都在进行这样的无限循环来判断自己是否是head之后的第一个有效的结点,如果是,则尝试去获取lock,不是则继续循环(CAS的体现)。
比如队列中第3个结点与第5个结点,两者都在进行循环,尝试将两者间的无效(cancel)的结点去掉,假设第4个结点的state为cancel,那么第3个结点自然就会与第5个结点连接。再假设第3个结点的前面两个结点都是有效结点,那么在判断p==head的时候返回false,所以继续循环,其实这是的循环我感觉已经是某种意义上的无用循环了,因为第二个结点状态始终有效。但是一旦找到在该节点前的第一个有效结点后,会使当前线程进入WAITTING的状态,等待unpark()或interrupt()使线程恢复,也就是说,哪怕此时结点是head后的第一个结点,也拿到了lock,但是线程却处于WAITTING状态,所以需要selfInterrupt();来触发一个中断,来恢复线程状态!
概括的流程:
第一次获取(失败)---> 第二次获取(失败)---> 插入队列---> 进入死循环 ---> 有该结点向前找,找第一个有效结点 ---> 找到后,当前线程挂起,进入WAITTING ---> 判断当前结点的前一个是否为head,若是则尝试获取lock ---> 获取到锁,interrupt(),是当前线程从中断中恢复。
lock()的总结:
1.线程未获取到锁进入队列后的操作(死循环在做什么)
2.线程从开始获取锁到进入阻塞队列的过程(3次获取锁的操作,体现非公平性)
3.多次的CampareAndSet操作,达到的目的
3.乐观锁的设计方式,体现在哪里
关于tryLock()
内部实际是调用了nonfairTryAcquire(1);所以实现原理上几乎一样
关于tryLock(long timeout, TimeUnit unit);
从函数名上就可以知道,这是自带超时机制的获取方式,在超出既定响应时间后没有获取到lock,抛出InterruptException,内部实现原理类似,但是多了计时机制。
3.释放锁的过程
1.unlock();
2.tryRelease();
unlock();
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;
}
tryRelease其实做的是设置锁的state,比较容易看出,判断当前线程是否为锁的拥有者,若是,锁的计数器-1,若是计数器为0,则清空锁的拥有者,因为ReentrantLock是排他锁,但是也是重入锁,所以允许一个线程多次获取锁,基于这个情况,只有lock的次数与unlock的次数对应,才能释放掉锁。
release做的就是释放锁之后唤醒head之后的第一个有效结点的线程。
因为在设计的时候任务head之后的第一个有效结点是有资格去竞争锁的,那么head一般就认为是已经完成任务的线程所在的结点(当然,不这样认为,仅仅认为是一个head指针也未尝不可)。
总结下unlock()
锁计数器-1 ---> 若计数器为0 ---> 释放锁,否则不释放 ---> 将head之后的第一个有效结点唤醒
重点:
1.ReentrantLock存在重入,所以需要判断锁计数器是否为0.
2.释放锁其实做的是lock的state置0,owner置null。
3.释放锁之后,将head之后第一个有效结点唤醒。
以上就是对于ReentrantLock的个人理解
1.核心部分
AbstractQueuedSynchronizer 核心操作类(不仅仅是在Lock领域,线程池也多有应用,俗称AQS)
Sync 继承AQS,提供了对Lock的部分操作
NonfairSync 继承Sync,见名知意,非公平锁的操作(默认为非公平所)
FairSync 继承Sync,见名知意,公平锁的操作
2.获取锁的过程
这里默认使用非公平锁(公平锁逻辑上类似。细节有所不同)
===========================================
方式1:lock();
方式2:tryLock();
方式3:tryLock(long timeout, TimeUnit unit);
===========================================
方式1:lock();
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
个人认为比较好理解,首先进行一个原子操作compareAndSetState(0, 1),lock的state在初始状态或者未被占有的状态下为0,这里是一种基于乐观锁的设计。
若是成功设置lock的state,设置当前线程为lock的owner。
否者进入acquire(1);
acquire(1);
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
官方释义:以独占模式来获取锁,且忽略中断。若成功获取锁,返回。否者有可能进入队列等待。
if判断条件分为2个,分别解析。
(1)tryAcquire(arg)
定义于AQS内,实现在ReentrantLock的NonfairLock中
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(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;
}
首先获取当前线程,判断当前lock的state(万一在我们第一次获取锁失败的时候刚好有人释放了呢),若lock的state是0,则尝试再去取compareAndSetState(0, acquires),成功返回true,否则返回false。
若是state!=0,两种情况。1.该线程本身就是lock的持有者,由于ReentrantLock支持重入,所以在符合最大重入次数的条件下,设置state++,返回true。
2.该线程不是lock的持有者,那么相当于又一次尝试获取锁失败。返回false.
tips:我认为这里能体现部分作为“非公平所”的特性,就是说,一个线程在第一次获取锁失败的情况下,还有第二次获取的机会,假设他与一个在等待队列中等待了许久的线程竞争,结果他回去了锁,是不是就意味着不公平?
再看acquire(int arg)的if语句中的2个条件,如果第一个tryAcquire返回的是true,根据短路与的特性,后面的都不执行,线程成功的获取lock,返回。
若是返回的是false,我们继续看接下来的条件
(2)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
先看addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
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)) {//设置新的tail结点
pred.next = node; //由于是双向链表,所以要设置prev以及next
return node;
}
}
enq(node);
return node;
}
首先,根据函数名就能猜到,该操作的结果是产生一个等待着,也就是使当前线程加入等待队列。
这里介绍下内部等待队列的数据结构。
注意:是一个双向链表,这里并没有标出。
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
Node node = new Node(Thread.currentThread(), mode);可以看到根据传入的Node的mode来创建出一个node,该node的内部thread指向当前线程。
tail指向尾节点,head指向头结点,可以认为,在第一个线程进入等待队列时,tail以及head都是空的。
所以先来关注enq(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;
}
}
}
}
我们来看,第一个将要等待队列的线程到来时,t必然为null。所以第一次会触发compareAndSetHead(new Node()),结果也就是产生一个辅助节点作为head节点。
然后我们需要关注的是这是一个for(;;),也就是死循环,第二次循环时,由于head节点已经被创建,那么node就会被插入到链表尾部,也就是prev指向前一个节点,tail指向该node。
说白了就是若等待队列为空,构造出一个等待队列(辅助的head结点,tail也指向head),再将传入的node插入队列。移动tail指针。
回过头去看addWaiter(Node.EXCLUSIVE)
其实做的只是插入一个结点,并且移动tail指向新插入的结点。
再来看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); //获取前一个结点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
要理解 if (p == head && tryAcquire(arg)),由于p是传入结点的前一个结点,如果p是head,则再次尝试去获取锁,如果获取到了,开始setHead(node)操作。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead(node);做的只是将head指向该node,同时置空一些node的信息,因为他已经获取到锁了,所以不需要再等待队列中了。
由此可见,在等待队列中最有可能能回去lock的是head后的第一个结点。
若是if (p == head && tryAcquire(arg))不满足,则进入 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) 分支。
shouldParkAfterFailedAcquire(p, node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) { //大于0 表示已经被cancel掉了 也就是无效的了
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这段代码不是很直面。
首先waitStatus的含义是指该结点内thread的等待状态,由以下几种组成:
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1; //表示该线程已取消
static final int SIGNAL = -1; //标示该线程需要被挂起
static final int CONDITION = -2; //标示该线程正在等待信号量
static final int PROPAGATE = -3; //等待共享锁
if (ws > 0) 若是满足此条件,从该结点开始,向前扫描,找到第一个状态不为cancel的结点。然后与找到的结点连接(next,prev),若是下一个结点的状态小于0,则尝试将其置为SIGNAL。
在关注外部的for(;;)是一个死循环,也就是说,第二次来遍历的时候必然会发现状态为SIGNAL的结点(不为cancel)。找到后返回true。
parkAndCheckInterrupt()
该操作只是将当前线程挂起到WAITING状态,等待一个中断或者unpark()方法来唤醒他。
若是if操作成功,则将Interrupt的值设置为true,表示需要中断来唤醒。
再回到顶层
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
假设此时tryAcquire(arg);没有获取到锁,return false; acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 返回false 说明等待队列的head之后的第一个结点已获取锁,且Interrupt的值为false,代表不需要中断操作来唤醒。若返回true,则说明队列中有一个结点已经获取锁,但是需要唤醒当前线程。
所以selfInterrupt();该操作的目的就是产生一个中断,是线程从WAITING中恢复。
具象一点的说法,假设现有N个线程未获取lock进入到了等待队列,每个线程都在进行这样的无限循环来判断自己是否是head之后的第一个有效的结点,如果是,则尝试去获取lock,不是则继续循环(CAS的体现)。
比如队列中第3个结点与第5个结点,两者都在进行循环,尝试将两者间的无效(cancel)的结点去掉,假设第4个结点的state为cancel,那么第3个结点自然就会与第5个结点连接。再假设第3个结点的前面两个结点都是有效结点,那么在判断p==head的时候返回false,所以继续循环,其实这是的循环我感觉已经是某种意义上的无用循环了,因为第二个结点状态始终有效。但是一旦找到在该节点前的第一个有效结点后,会使当前线程进入WAITTING的状态,等待unpark()或interrupt()使线程恢复,也就是说,哪怕此时结点是head后的第一个结点,也拿到了lock,但是线程却处于WAITTING状态,所以需要selfInterrupt();来触发一个中断,来恢复线程状态!
概括的流程:
第一次获取(失败)---> 第二次获取(失败)---> 插入队列---> 进入死循环 ---> 有该结点向前找,找第一个有效结点 ---> 找到后,当前线程挂起,进入WAITTING ---> 判断当前结点的前一个是否为head,若是则尝试获取lock ---> 获取到锁,interrupt(),是当前线程从中断中恢复。
lock()的总结:
1.线程未获取到锁进入队列后的操作(死循环在做什么)
2.线程从开始获取锁到进入阻塞队列的过程(3次获取锁的操作,体现非公平性)
3.多次的CampareAndSet操作,达到的目的
3.乐观锁的设计方式,体现在哪里
关于tryLock()
内部实际是调用了nonfairTryAcquire(1);所以实现原理上几乎一样
关于tryLock(long timeout, TimeUnit unit);
从函数名上就可以知道,这是自带超时机制的获取方式,在超出既定响应时间后没有获取到lock,抛出InterruptException,内部实现原理类似,但是多了计时机制。
3.释放锁的过程
1.unlock();
2.tryRelease();
unlock();
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;
}
tryRelease其实做的是设置锁的state,比较容易看出,判断当前线程是否为锁的拥有者,若是,锁的计数器-1,若是计数器为0,则清空锁的拥有者,因为ReentrantLock是排他锁,但是也是重入锁,所以允许一个线程多次获取锁,基于这个情况,只有lock的次数与unlock的次数对应,才能释放掉锁。
release做的就是释放锁之后唤醒head之后的第一个有效结点的线程。
因为在设计的时候任务head之后的第一个有效结点是有资格去竞争锁的,那么head一般就认为是已经完成任务的线程所在的结点(当然,不这样认为,仅仅认为是一个head指针也未尝不可)。
总结下unlock()
锁计数器-1 ---> 若计数器为0 ---> 释放锁,否则不释放 ---> 将head之后的第一个有效结点唤醒
重点:
1.ReentrantLock存在重入,所以需要判断锁计数器是否为0.
2.释放锁其实做的是lock的state置0,owner置null。
3.释放锁之后,将head之后第一个有效结点唤醒。
以上就是对于ReentrantLock的个人理解