目录
死锁
ReentrantLock与Synchronized对比
源码分析
Lock接口
lock()实现
NonfairSync
tryAcquire()
addWaiter()
acquireQueued()
FairSync
tryAcquire()
NonfairSync和FairSync的本质区别
tryLock()实现
unlock()实现
Condition实现
await()
signal()和signalAll()
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都将无法继续执行下去。最经典的问题就是哲学家就餐问题:
假设有五位哲学家围坐在圆桌旁,进行思考和进餐。每位哲学家面前都有一碗面条,而吃面条需要用到两只筷子。每位哲学家的左右两边各有一只筷子,总共就是五只筷子。
哲学家只有在拿到左右两边的筷子后才能吃面,吃完后再把筷子放下,继续思考。 这个问题的关键在于,如果每位哲学家都先拿起自己左边的筷子,然后等待右边的筷子,那么就会出现死锁的情况,因为每位哲学家都在等待别人放下筷子,但是没有人会放下筷子。
解决死锁问题的方法有以下几种:
1、按照顺序加锁:可以针对筷子进行编号,所有哲学家必须按照编号从小到大获取筷子。
2、尝试加锁:利用trylock的思想,先拿起一根筷子,然后尝试拿下一个筷子,如果拿到了就吃饭,如果拿不到,就释放所有的筷子。
3、超时机制:让线程不要一直等待,一段时间内不能满足要求,则放弃所有的资源。
Synchronized详细看:三、详解Synchronized-CSDN博客
ReentrantLock是Java并发包java.util.concurrent.locks中的一个类,它实现了Lock接口,提供了与synchronized关键字类似的互斥锁功能。ReentrantLock的名字来源于它是一个可重入锁,也就是说,一个线程可以多次获取同一个ReentrantLock锁,每次获取锁时,计数器会递增,每次释放锁时,计数器会递减,当计数器为0时,锁被释放。
ReentrantLock相比于synchronized关键字,提供了更高的灵活性和更多的功能,例如:
1. 可以显式地获取和释放锁,这使得锁的范围更加灵活,可以跨越多个方法或代码块。
2. 支持公平锁和非公平锁。公平锁是指等待时间最长的线程将优先获得锁,非公平锁则没有这个限制。默认情况下,ReentrantLock是非公平锁,但可以在构造函数中传入参数true来创建公平锁。
3. 提供了tryLock()方法,可以尝试获取锁,如果获取不到锁,线程可以继续执行其他任务,而不是一直等待。
4. 支持中断。当一个线程在等待锁时,可以被其他线程中断,这样可以避免死锁问题。
一个使用ReentrantLock的使用demo:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void method1() {
lock.lock(); // 获取锁, 此时当前线程会一直等待,直到获取锁为止
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
}
public void method2() {
if (lock.tryLock()) { // 尝试获取锁,可能成功,也可能失败
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
} else {
// 未获取到锁,执行其他任务
}
}
}
想要学习ReentrantLock的源码,就必须先搞明白ReentrantLock类的继承关系。在idea上查看继承关系如下:
从继承关系上来看,ReentrantLock也就是简单的实现了Lock接口。那我们就先看一下这个Lock接口到底都包含啥:
public interface Lock {
/**
加锁
*/
void lock();
/**
可打断加锁
*/
void lockInterruptibly() throws InterruptedException;
/**
尝试加锁
*/
boolean tryLock();
/**
在一定时间范围内尝试加锁
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
解锁
*/
void unlock();
/**
返回一个锁的条件
*/
Condition newCondition();
}
从上面的代码来看,Lock的接口提供的功能非常简单就是加锁、解锁、还有一个类似于wait方法的条件。
那么接下来看一下,在ReentrantLock中,是如何实现这个Lock接口的。
首先看一下核心的lock()方法是如何实现的呢?我们定位到ReentrantLock的lock()方法:
public void lock() {
sync.acquire(1);
}
发现内部实际上是调用了一个变量sync的acquire方法。那么这个sync变量是啥呢?什么时候给赋值的呢?
跟踪代码,看到sync是ReentrantLock类中的一个属性变量,而属性变量一般都会在构造方法中赋值,因此,查看ReentrantLock的构造方法:
/**
*返回一个非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
*返回公平锁或者非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
这里可以看出来,当我们执行构造方法时,默认创建了一个NonfairSync对象。也就是非公平锁,
那么具体什么是公平锁,什么是非公平锁,这里简单描述就是:公平指的是先来的线程先获取到锁,后来的线程后获取到锁。而非公平指的是不管先来后来,后来的锁也可以先获取到锁。后面我们详细解释是如何做到的。
好了,从上面的构造函数来看,有多了 FairSync和NonfairSync这个两个类。我们还是先看一下这个两个类的继承结构。
FairSync:
NonfairSync:
从图上看,这个两个类的继承关系一样,那么为什么需要设置两个类呢?这个两个类到底哪些地方不同呢?
我们先来分析一下非公平锁,这个也是ReentrantLock默认的实现。当调用lock()方法的时候,实际上是调用了NonfairSync.acquire()
具体看NonfairSync.acquire() 是什么逻辑,这个acquire在NonfairSync类中本身是没有提供的,但是由于他继承了父类,那么acquire肯定在其父类AbstractQueueSynChronizer。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在这个方法中,有三个重要的方法:
1、tryAcquire :尝试去获取, 如果为true,则直接返回,那就意味着lock方法返回,继续执行,
如果为false,那么执行addWaiter方法
2、addWaiter:将线程添加到同步
3、acquireQueued:从同步队列中获取一个线程来执行
那么我们先看tryAcquire是干啥的,继续跟踪,发现这个方法居然是protected的,这就意味着这个方法应该交由子类实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
而当前AQS的子类就是我们的NonfairSync。那么就看一下NonfairSync中的tryAcquire到底是干啥呢?
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;
}
我们尝试理解一下这段逻辑:
1、获取当前执行的线程,这个线程就是一开始调用lock.lock()那个线程。
2、获取状态,
3、如果状态为0, 那么就尝试利用cas的方式将0设置为acquire,这里acquire就是1(画外音:利用state为0表示无锁,<0表示有锁。)
4、如果说cas成功,那么就设置ExclusiveOwnerThread为自己当前线程,然后返回true。(画外音:如果cas成功,那么意味加锁成功,并且将【持有锁线程】设置为当前线程,然后返回true)
5、如果说状态不是0,那么判断ExclusiveOwnerThread是否为自己,如果是自己那么就将状态值加1 (画外音:如果现在已经有别的线程加了锁,那么先判断一下,【持有锁线程】是否为自己,如果为自己,那么就将state累计加1,表示当前线程重入的次数)
6、如果上述都不满足则返回false。(画外音:如果有线程加锁,而且加锁的线程不是当前线程,那么就返回false)
接下来看一下addWaiter方法。
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();
}
}
}
尝试分析:
1、先创建一个node
2、不断死循环,判断同步队列是否有一个队尾,如果有,则利用cas将当前的node插入到同步队列,之后break。
3、如果原始同步队列没有队尾,那么就初始化一个队列,然后再次进入循环。
好,到此为止,我们整理一下,lock方法到底干了啥:
1、先尝试加锁,也就是cas去设置state,如果加锁成功,那么就执行lock之后的代码,如果加锁失败,则创建一个node,并加入到同步队列。
最后一步,执行acquireQueued(), 继续跟踪这个方法的逻辑:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
继续分析:
1、进入一个死循环
2、获取到刚刚新创建的node的前驱节点。如果说前驱节点为head,那么就去再次执行tryAcquire()。(画外音:如果前驱节点为head,那就意味着当前新创建的node前面没有任何被阻塞的节点了,那么当前线程就应该去继续尝试加锁)
3、如果加锁成功了,则把当前node设置为head,然后返回。注意这里的返回,意味着lock方法已经结束了,可以执行lock里面的临界区代码了
4、如果说当前node的前驱节点不是head,那么当前线程就会被park住,等待。这就意味着lock方法被阻塞,线程等待锁。
到此,应该彻底明白了lock方法到底都干啥了:
1、尝试加锁(tryAcquire),如果成功,则直接返回,执行lock后面的代码。
2、如果失败,将node添加到同步队列(addWaiter)
3、之后当前线程一直死循环去判断当前node是否为head,如果为head,继续尝试加锁(tryAcquire),如果成功则直接返回,执行lock后面代码。如果失败,那么就被park,等待其他线程唤醒。
上面分析了非公平锁的加锁逻辑,现在看看公平锁的加锁逻辑,到底是如何做到公平的。
跟踪一下这个公平的锁tryAcquire()
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;
}
继续分析:
1、获取当前线程
2、是否有锁,如果没有锁,先判断当前等待队里中是否已经有了node。如果说有了node,那么就直接返回加锁失败,如果之前没有node,则尝试加锁,并设置【锁持有线程】
3、如果有锁,判断【持有锁的线程】是否为当前线程,如果是,则state加1,返回加锁成功
4、如果有锁且不是当前线程持有锁,那么返回加锁失败。
对比两个类的traAcquire方法可以看出来,当发现state=0(没有锁的时候),两个类的加锁方式不同:
1、非公平:当前线程直接尝试加锁
2、公平:先看一下,同步队列中是否已经有其他线程在等待锁,如果有,那么当前线程放弃加锁,直接去排队。如果没有,才开始加锁。
tryLock和lock本质的区别就是:trylock一次尝试加锁,不论成功与否就直接返回了,而lock多次加锁,成功就返回,失败了就死循环一个尝试。
public boolean tryLock() {
return sync.nonfairTryAcquire(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;
}
可以看到tryLock的方法中直接就调用了tryAcquire,而不需要调用addWaiter和accquireQueued。因此改方法成功与否都会直接返回。
unlock方法就是解锁,解锁的话就不需要区分公平不公平了,因为执行解锁的线程就是当前持有锁的线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
尝试分析一下:
1、调用tryRelease方法。这个方法也是一个需要被子类实现的方法。这个道理很简单,因为tryAcquire方法是子类实现的,那么解锁的tryRelease的方法也应该是子类实现。因为不通子类加锁和解锁的逻辑是不一样的。
2、获取到head节点,unpark等待的其他线程。
看一下是如何进行tryRelease的:
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;
}
分析:
1、将锁的重入次数减去release,当前release=1。意味着当前持有线程的重入减1
2、判断当前【锁持有线程】是否和当前线程一样,如果不一样那就报错,这里一般来说肯定是一样的,因为只有lock加锁成功的线程才会向下执行,才会执行到unlock方法。
3、如果state=0,将【锁持有线程】设置为null。意味着没有锁了
4、之后直接setState,这里没有用cas,因为解锁的逻辑只有一个线程执行,不会冲突。
当前线程解锁之后,如何通知其他等待的线程继续来抢锁呢?答案就是在unparkSuccessor方法中。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//知道这里为什么唤醒的node.next吗,因此node是当前线程,next才是要被唤醒的线程。
//具体可以看一下,addWaiter()方法,初始的时候head是一个dummyNode,
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}
核心的逻辑就是LockSupport.unpark()。 当此行代码执行,那么在同步队列中等待的第一个node中的线程将被唤醒,然后执行accquireQueued中的那个死循环。
到此为止,ReentrantLock的加锁和解锁就讲完了。总结:
1、ReentrantLock中有两种锁,一种公平,一种非公平,默认是非公平。两者的区别就是在加锁的时候判断是否需要排队。
2、加锁的时候,利用cas将一个state变量设置为1,然后将【锁持有线程】设置为自己。重入的时候,将state累计加1.
3、加锁失败了之后,会封装成一个node,然后将node插入到同步队列的尾部。之后执行for循环,一直不断地尝试加锁,如果加锁失败,则park住,让出cpu。
4、解锁流程就是将state不断地减1,一旦state减为0,那就意味着线程所有重入逻辑结束,直接释放【锁持有线程】,并唤醒同步队列中的其他线程。
Condition是一个接口主要包含了一下几个方法:
public interface Condition {
/**
等待
*/
void await() throws InterruptedException;
/**
打断等待
*/
void awaitUninterruptibly();
/**
超时等待
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
超时等待
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
唤醒
*/
void signal();
/**
全部唤醒
*/
void signalAll();
}
这个方法用于使当前线程等待,直到它被其他线程唤醒,或者被中断。
常见的方法为 await()和signal()。在调用await()方法之前,线程必须持有与Condition相关联的Lock。调用await()方法后,线程会释放这个锁,并进入等待状态。当其他线程调用Condition的signal()或signalAll()方法时,等待的线程之一会被唤醒并重新获取锁。
下面给一个简单demo:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionMet()) {
condition.await(); //等待
}
// 执行一些操作
} catch (InterruptedException e) {
// 处理中断
} finally {
lock.unlock();
}
我们首先分析一下await方法输入
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//将当前节点加入到等待队里
Node node = addConditionWaiter();
//释放当前线程的所有锁资源
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//挂起当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//当线程被唤醒之后,开始尝试获取锁,进行重新加锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
分析这段逻辑:
1、先将自己的node加入到等待队列中。这里流程和链表添加node一样,不需要加锁,因为此时只有持有锁的线程才会执行到这一步
2、释放当前线程的所有锁资源。在这里,释放资源之后,会唤醒其他同步队里的线程。
3、将当前线程park住
4、如果当前线程被其他线程unpark,则开始重新尝试加锁(acquireQueued方法)
这里有几个细节需要注意:
1、当线程被唤醒之后,为什么没有看到 addWaiter()这一步呢。acquireQueued方法只是循环去判断当前node是否为head,但是什么时候node被迁移到同步队列中去的呢?答案在signal中。
2、当fullyRelease(node)返回了savedState表示当前线程重入了几次。当释放完所有资源之后,state=0,唤醒其他线程去抢锁。当当前线程被唤醒之后,去重新加锁的时候将savedState传递给了acquireQueued方法,使得state又变成了原来的数值
这两个方法的本质区别在于是唤醒一个线程,还是唤醒所有线程。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
对比两者之间的逻辑:
singalAll:先唤醒当前node,然后while里面在唤醒next,一直循环,直到队尾
signal:先将当前节点和队列断开,然后去唤醒,如果唤醒成功,则直接break这个while。如果唤醒失败,那么继续唤醒等待队列中的下一个节点,直到成功唤醒一个节点或等待队列为空。
重点看一下这个transferForSignal方法:
final boolean transferForSignal(Node node) {
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
//这一步解决之前的疑问,signal的时候不单单去执行唤醒,还执行插入同步队列
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread); //唤醒当前线程
return true;
}
这个方法先利用循环cas的方式将node迁移到同步队列中,然后再唤醒当前线程。之后线程的执行逻辑就是从await方法的park函数返回,执行加锁逻辑。