ReentrantLock阅读笔记
文章目录
问题
1、为什么默认是非公平模式
- 因为效率比公平模式高
2、为什么非公平效率高
- 因为非公平锁会在开始就尝试2次获取锁,少了排队导致的阻塞/等待过程,减少了线程频繁的切换带来的性能开销
3、非公平模式的弊端
- 非公平模式下可能导致一开始排队的线程一直获取不到锁,导致线程饿死。
一、简介
此内容仅仅含有公平模式和非公平模式,不含条件锁源码
二、继承关系图
是Lock的实现类,Lock是一个接口,
public interface Lock {
// 获得锁
void lock();
// 获得锁,可中断
void lockInterruptibly() throws InterruptedException;
// 如果锁处于等待,则获取锁
boolean tryLock();
// 尝试获取锁,如果没有获取到锁,就等待一段时间,最后还是没有获得,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 条件锁
Condition newCondition();
}
三、存储结构
四、源码分析
内部类
属性
private final Sync sync;//锁,有2个子类:FairSync 和 NonfairSync
构造
/** 默认构造,默认创建非公平锁 */
public ReentrantLock() {
sync = new NonfairSync();
}
/** fair:true实例化公平锁,false实例化非公平锁 */
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
主要方法
1、加锁源码(公平模式),
-
问题:只是有一个问题,再加锁过程中正好有线程释放锁,获取锁成功,不就插队了??
-
代码调用流程 如下
/** ReentrantLock#lock() 加锁 --ReentrantLock.FairSync#lock() 用FairSync加锁 ----AbstractQueuedSynchronizer#acquire() 调用AQS中的acquire加锁,传值1 ------ReentrantLock.FairSync#tryAcquire() 获取锁,如果是重入,则status + 1 返回,否则入队 ------AbstractQueuedSynchronizer#addWaiter() 上面获取锁失败,则把锁加入队列, --------AbstractQueuedSynchronizer#enq() 初始化队列 或 上面加入队列失败 ------AbstractQueuedSynchronizer#acquireQueued() 自己是第一个节点就继续for(;;)获取锁,否则中断 --------AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() 前节点是否是SIGNAL --------AbstractQueuedSynchronizer#parkAndCheckInterrupt() //阻断线程 */
-
详细加锁代码
/** private final Sync sync = new FairSync(); */
//ReentrantLock.lock()
public void lock() {
sync.lock();
}
//ReentrantLock.FairSync.lock()
final void lock() {
acquire(1);
}
//AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
//1、尝试获取锁,成功则结束,否则执行2
//2、添加等待队列,然后再尝试获取一次锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//没有获取锁,则让当前线程处于等待Thread.currentThread().interrupt();
selfInterrupt();
}
//ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果是0,说明当前没有线程占有锁,开始尝试获取锁
if (c == 0) {
//hasQueuedPredecessors AQS查询是否有线程等待的时间比当前线程长
//compareAndSetState AQS获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//当前获取了锁就把自己放入独占线程中
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程本身占有了锁,那么直接让它获取,并且返回true
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//重入次数+1
if (nextc < 0)//如果溢出,报错
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//AbstractQueuedSynchronizer.addWaiter()
private Node addWaiter(Node mode) {
//新建一个节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//这里先把新节点加入尾节点,如果成功了则返回新节点,失败则调用enq()不断尝试
Node pred = tail;
//如果节点不为null
if (pred != null) {
//设置新节点的前置节点为线程的尾节点
node.prev = pred;
//cas更新尾节点为新节点
if (compareAndSetTail(pred, node)) {
//如果成功了。把旧尾节点的下一个节点设置为新节点
pred.next = node;
//返回新的尾节点
return node;
}
}
// 如果上面尝试入队新节点没有成功 或队列还没初始化,则调用enq cas替换
enq(node);
//返回新的node节点
return node;
}
//AbstractQueuedSynchronizer.enq()
private Node enq(final Node node) {
for (;;) {
//取出尾节点
Node t = tail;
//如果尾节点为null,说明没有初始化队列,则初始化队列
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//尾节点不为null,则设置新的节点为现在的尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
//返回旧的尾节点
return t;
}
}
}
}
//AbstractQueuedSynchronizer.acquireQueued()
//调用上面的addWaiter,使得新节点已经加入到队列
//这个方法是尝试用新节点来获取锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//表示获取锁失败,true失败,flase成功
try {
boolean interrupted = false;
for (;;) {
//获得当前节点的上一个节点
final Node p = node.predecessor();
//如果自己是队列中第一个线程节点,就再次尝试获取锁
if (p == head && tryAcquire(arg)) {
//获取成功,此处node是new Node(),就设置head
setHead(node);
p.next = null; // help GC
failed = false;//获取锁没有失败
return interrupted;//不用阻塞线程
}
//如果获取锁失败,是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
//前面返回true,说明需要中断方法
parkAndCheckInterrupt())
//如果中断成功
interrupted = true;
}
} finally {
//如果获取锁失败了
if (failed)
//取消获取锁
cancelAcquire(node);
}
}
// AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
// 是否应该阻塞获取锁失败的节点:p是当前节点的前一个节点,node是当前节点
// 在上面的for循环中调用的, 第一次只会把前一个节点设置为SINGNAL,然后返回false
// 第二次调用才会返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//注意:node节点的waitStatus字段我们之前创建node的时候并没有指定
// 也就是说没有指定的话,默认是0
//static final int CANCELLED = 1; 线程取消
//static final int SIGNAL = -1; 等待唤醒
//static final int CONDITION = -2; 条件锁使用
//static final int PROPAGATE = -3; 共享锁使用
int ws = pred.waitStatus;//获取前一个节点的状态
//如果前一个节点的等待状态是SIGNAL(等待唤醒),则返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
//如果前一个节点的状态大于0,也就是取消状态
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
// 把前面所有取消状态的节点都从链表中删除
do {
node.prev = pred = pred.prev;//删除已取消节点
} while (pred.waitStatus > 0);//再次判断前节点是否是取消节点
pred.next = node;//删除了所有前面取消节点,然后关联上node
} else {
/*
* 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.
*/
//如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒,
//此处可以直接理解为把初始化状态0设置为SIGNAL
//CONDITION是条件锁的时候使用的
//PROPAGATE是共享锁的时候使用
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//AbstractQueuedSynchronizer.parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程,调用的是Unsafe中的park方法
LockSupport.park(this);
//返回是否已中断
return Thread.interrupted();
}
总结:(借用彤哥的文笔,我文笔太弱了)
获取锁的主要过程大致如下:
(1)尝试获取锁,如果获取到了就直接返回了;
(2)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
(3)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
(4)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);
(5)调用parkAndCheckInterrupt()阻塞当前线程;
(6)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
(7)如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。
以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。
2、加锁源码(非公平模式)
经过阅读公平锁的代码,其实大家都知道加锁会调用Sync实现里面的lock
和tryAcquire
方法 这里区分了公平和非公平,其他地方都一样的。
- 详细加锁代码
/** private final Sync sync = new NonFairSync(); */
//ReentrantLock.lock()
public void lock() {
sync.lock();
}
//ReentrantLock.NonFairSync.lock()
final void lock() {
//不公平嘛,直接来获取锁,不管队列有没有其他线程,获取成功直接设置自己为主人
if (compareAndSetState(0, 1))//
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//ReentrantLock.NonFairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
//调用的父类Sync中的nonfairTryAcquire方法
return nonfairTryAcquire方法(acquires);
}
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 和公平锁的不同,是没有判断队列是否有比自己等待时间还长的线程(!hasQueuedPredecessors()),
// 直接抢占一次锁
if (compareAndSetState(0, acquires)) {
//抢占锁成功
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程重新加锁,就直接status+1 (下面和公平锁一样,没啥区别都是重入)
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;
}
总结:
和公平锁的区别就2点:
- 第一次直接抢占锁一次 ,公平锁会判断是否是第一个节点
- 锁加入队列后,再判断status == 0 ,是就抢占锁一次。公平锁会用 !hasQueuedPredecessors()判断
3、lockInterruptibly()
对线程打上一个中断标记,不会对线程运行造成什么影响,具体用来干嘛,由用户自己决定
比如:如果用户在调用lock()获取锁后,发现线程中断了,就直接返回了,而导致没有释放锁,这也是允许的,但是会导致这个锁一直得不到释放,就出现了死锁。
//ReentrantLock.lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {
//设置线程为中断状态
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //如果线程本身就是中断状态则InterruptedException异常
throw new InterruptedException();
if (!tryAcquire(arg))//尝试获取一次锁,多态调用非公平或公平方法
//获取失败 arg = 1
doAcquireInterruptibly(arg);
}
// 相当于加锁中的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的组合体
// 唯一不同的是 不用返回是否阻断标记,如如果加锁不成功就直接抛出中断异常
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//直接跑异常,不用和加锁一样返回中断标记了
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4、tryLock()
//实现很简单,直接调用的Sync.nonfairTryAcquire(1);成功true,不成false,
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
5、tryLock(long timeout,TimeUnit unit);
- 在阻塞的时候加上阻塞时间,并且会随时检查是否到期,只要到期了没有获取到锁就返回false
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
//直接调用ReentrantLock.Sync类的AQS父类的方法
//unit.toNanos 转为纳秒,1毫秒等于1000微妙,1微妙 = 1000纳秒
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// AQS中的方法 arg = 1 nanosTimeOut = tomeout 直接
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())//当前线程是中断就抛出InterruptedException异常
throw new InterruptedException();
// 尝试获取锁,成功返回true
// 尝试获取锁失败,
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
// AQS中的方法
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果时间到期了则返回
if (nanosTimeout <= 0L)
return false;
//计算循环结束的纳秒时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;//获取锁标记,默认为获取锁失败
try {
for (;;) {
//如果是第一个节点就尝试获取锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//计算循环剩余纳秒
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) //达到超时,返回false
return false;
// 设置之前节点为SIGNAL(会处理掉前节点所有线程取消状态的节点)
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果剩余 纳秒 > 1000L 就阻塞
// 剩余小于等于1000 就自旋解决
nanosTimeout > spinForTimeoutThreshold)
//阻塞线程剩余 纳秒时间
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
//获取锁失败
if (failed)
//取消获取锁
cancelAcquire(node);
}
}
6、unlock()
- 将status 的值减 1; 如果state减到了0,说明完全释放了锁,则唤醒下一个等待的节点
//释放锁
public void unlock() {
//调用内部类Sync中的AQS父类release方法的释放锁
sync.release(1);
}
//AbstractQueuedSynchronizer.releas()
public final boolean release(int arg) {
//调用子类的方法,此处是Sync类
if (tryRelease(arg)) {
Node h = head;
//如果头节点不为null 且 等待状态不为0,就唤醒下一个节点
// 还记得waitStatus吗?
// 在每个节点阻塞之前会把其上一个节点的等待状态设置为SIGNAL(-1)
// 所以SIGNAL 的准确理解应该是唤醒下一个等待的线程
if (h != null && h.waitStatus != 0)
//唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
//ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是 占有着锁的线程,抛出IllegalMonitorStateException异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 如果状态为0了 就直接清空占有线程(不清空会导致之前线程永远无法获取到锁)
setExclusiveOwnerThread(null);
}
// 设置状态变量
setState(c);
return free;
}
// 唤醒node之后的节点,如果有的话,
//AbstractQueuedSynchronizer.unparkSuccessor()
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;
// 如果节点的等待状态小于0 就设置为 0
if (ws < 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;
// 就去尾节点递归向上找 到waitStatus <=0 的最新等待唤醒节点。
if (s == null || s.waitStatus > 0) {
// 说明s == null 或者 节点大于 0 (已取消状态)
s = null; //以免是取消状态
// 从尾节点向前遍历取到队列最前的那个状态不是已取消状态的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 通过上面的 逻辑 s 还不为null 就说明找到了。直接唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
补充:条件锁源码
参考:直接套老彤的文章
条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。
比如,在阻塞队列中,当队列中没有元素的时候是无法弹出一个元素的,这时候就需要阻塞在条件notEmpty上,等待其它线程往里面放入一个元素后,唤醒这个条件notEmpty,当前线程才可以继续去做“弹出一个元素”的行为。
注意,这里的条件,必须是在获取锁之后去等待,对应到ReentrantLock的条件锁,就是获取锁之后才能调用condition.await()方法。
在java中,条件锁的实现都在AQS的ConditionObject类中,ConditionObject实现了Condition接口,下面我们通过一个例子来进入到条件锁的学习中。
使用方式
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
// 声明一个重入锁
ReentrantLock lock = new ReentrantLock();
// 声明一个条件锁
Condition condition = lock.newCondition();
new Thread(() -> {
try {
lock.lock(); // 1
try {
System.out.println("before await"); // 2
// 等待条件
condition.await(); // 3
System.out.println("after await"); // 10
} finally {
lock.unlock(); // 11
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 这里睡1000ms是为了让上面的线程先获取到锁
Thread.sleep(1000);
lock.lock(); // 4
try {
// 这里睡2000ms代表这个线程执行业务需要的时间
Thread.sleep(2000); // 5
System.out.println("before signal"); // 6
// 通知条件已成立
condition.signal(); // 7
System.out.println("after signal"); // 8
} finally {
lock.unlock(); // 9
}
}
}
上面的代码很简单,一个线程等待条件,另一个线程通知条件已成立,后面的数字代表代码实际运行的顺序,如果你能把这个顺序看懂基本条件锁掌握得差不多了。
源码分析:无,有兴趣可以去分析。太累了
五、总结
无
来源:CSDN
作者:菜鸟编程98K
链接:https://blog.csdn.net/qq_39938758/article/details/103796192