ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。
相对于 synchronized, ReentrantLock具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
使用示例:
public class ReentrantLockDemo {
private static int sum = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(() -> {
//加锁
lock.lock();
try {
for (int j = 0; j < 10000; j++) {
sum++;
}
} finally {
// 解锁
lock.unlock();
}
});
thread.start();
}
Thread.sleep(2000);
System.out.println(sum);
}
}
1.非公平锁加锁
默认情况下是非公平锁:
public ReentrantLock() {
sync = new NonfairSync();
}
加锁源码如下:
public void lock() {
sync.lock();
}
来看看NonfairSync#lock:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
加锁逻辑:
- step1.这里首先会通过CAS尝试加锁,加锁成功则设置exclusiveOwnerThread为当前线程
- step2.如果CAS加锁失败,则进入acquire(1)方法处理
acquire是在AQS里面定义的核心逻辑:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire加锁逻辑:
- step1.tryAcquire(arg)尝试加锁,加锁成功则返回,加锁失败则继续后续逻辑
- step2.addWaiter将加锁失败的线程构建成成Node节点,加入到同步等待队列
- step3.acquireQueued这里会park线程,并且有线程被唤醒后尝试加锁的逻辑
注意上面addWaiter()和acquireQueued()逻辑AQS已经写好了,ReentrantLock主要实现了tryAcquire(),具体来看看这个方法
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;
}
从源码可以看出:
- step1.通过CAS操作尝试加锁,加锁成功则设置exclusiveOwnerThread为当前线程
- step2.实现了可重入锁加锁逻辑
2.非公平锁释放锁
这里非公平锁、公平锁都共用AQS里面的释放锁方法
public void unlock() {
sync.release(1);
}
调用AQS的释放锁方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放锁逻辑:
- step1.释放锁tryRelease(arg)
- step2.释放锁成功,则唤醒同步队列head的后继节点的线程
此时在acquireQueued阻塞线程就会醒来,如果是头节点的后继节点,就会尝试加锁,加锁成功则返回,加锁失败则继续park阻塞。
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);
}
}
另外,这里释放锁主要看ReentrantLock实现的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;
}
这里考虑到了可重入锁释放锁,只有当state减到0,才是真正释放锁成功,上面的release才能继续后面unpark唤醒阻塞的head后继节点的线程。
3.公平锁
加锁源码FairSync#lock:
final void lock() {
acquire(1);
}
这里与非公平锁的区别就少了一个CAS尝试加锁的步骤,直接调用的就是acqire(1).
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
公平锁FairSync#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;
}
从这里可以看出,只有当同步队列是空的时候,才会尝试加锁,否则就会直接入队(同步等待队列),根本不会尝试加锁,这就符合了公平锁的语义,遵守先来后到的顺序。
4.条件变量
调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。
使用示例:
/**
* 条件变量
*/
@Slf4j
public class ReentrantLockDemo6 {
private static ReentrantLock lock = new ReentrantLock();
private static Condition cigCon = lock.newCondition();
private static Condition takeCon = lock.newCondition();
private static boolean hashcig = false;
private static boolean hastakeout = false;
//送烟
public void cigratee() {
lock.lock();
try {
while (!hashcig) {
try {
log.debug("没有烟,歇一会");
cigCon.await();
} catch (Exception e) {
e.printStackTrace();
}
}
log.debug("有烟了,干活");
} finally {
lock.unlock();
}
}
//送外卖
public void takeout() {
lock.lock();
try {
while (!hastakeout) {
try {
log.debug("没有饭,歇一会");
takeCon.await();
} catch (Exception e) {
e.printStackTrace();
}
}
log.debug("有饭了,干活");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo6 test = new ReentrantLockDemo6();
new Thread(() -> {
test.cigratee();
}).start();
new Thread(() -> {
test.takeout();
}).start();
new Thread(() -> {
lock.lock();
try {
hashcig = true;
//唤醒送烟的等待线程
cigCon.signal();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
hastakeout = true;
//唤醒送饭的等待线程
takeCon.signal();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
4.1 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)addConditionWaiter会将当前线程构建成节点Node加入到条件等待队列
- 2)fullyRelease完全释放锁
- 3)阻塞线程
- 4)后续就是线程被唤醒后者中断的逻辑
唤醒后的逻辑有两种情况
- 1)signal或者signalAll()唤醒,此时节点已经转移到同步队列,就会跳出while (!isOnSyncQueue(node)) 循环,进入acquireQueued(node, savedState) 尝试加锁和阻塞逻辑
- 2)其他线程中断了该线程checkInterruptWhileWaiting(),此时会将该节点加入到同步队列,但是没有从条件队列移除,所以后面有个补偿逻辑:node.nextWaiter != null时,unlinkCancelledWaiters()会从条件队列中删除
4.2 signalAll()
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
核心逻辑:
- 1)将节点从条件队列移除
- 2)将节点加入到同步队列,并且唤醒线程,这里只有是取消节点或者cas设置节点状态为SIGNAL失败时,才会唤醒线程,其余情况不会
5.关于AQS
ReentrantLock就是基于AQS实现的。
AQS定义两种队列
- 同步等待队列: 主要用于维护获取锁失败时入队的线程
-
条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
可见ReentrantLock符合经典的MESA模型:
ReentrantLock只用实现共享资源state的获取与释放方式即可,具体线程等待队列(同步等待队列、条件等待队列)的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
ReentrantLock主要实现如下方法:
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。