使用ReentrantLock进行同步
ReentrantLock lock = new ReentrantLock(false);//创建lock实例,false为非公平锁,true为公平锁
lock.lock() //加锁
lock.unlock() //解锁
ReentrantLock是一种类似synchronized的互斥锁;需要手动加锁与解锁(显式锁);支持公平锁与非公平锁;可重入。 基于AQS框架的应用实现,通过定义内部类Sync来实现AbstractQueuedSynchronizer(AQS)。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}
...
static final class NonfairSync extends Sync {...}
static final class fairSync extends Sync {...}
...
}
AQS
继承关系
AbstractQueuedSynchronizer继承关系如下,除了Lock,Java.concurrent.util当中还有很多并发类都是基于AQS框架实现:
在ReentrantLock内部,还有两个子类继承了Sync,分别是NonfairSync、fairSync,在这两个类中实现了公平锁与非公平锁的逻辑。
核心属性
AbstractOwnableSynchronizer
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
...
//记录当前获取锁的线程。AbstractQueuedSynchronizer实现了该抽象类,所以也有该属性。
private transient Thread exclusiveOwnerThread;
...
AbstractQueuedSynchronizer
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
...
//AQS内部基于Node会构建一条双向链表(prev、next),也就是同步等待队列(CLH队列),存储等待中的线程
static final class Node {
//Node两种模式:共享 / 独占
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//Node四种信号量,初始状态为0
static final int CANCELLED = 1;//出现异常,需要废弃结束
static final int SIGNAL = -1;//可被唤醒
static final int CONDITION = -2;//条件等待
static final int PROPAGATE = -3;//传播
//初始为0
volatile int waitStatus;
//双向链表(prev、next)
volatile Node prev;
volatile Node next;
//Node保持对线程的引用
volatile Thread thread;
...
}
//指向同步等待队列的头节点,初始为null
private transient volatile Node head;
//指向同步等待队列的尾节点,初始为null
private transient volatile Node tail;
//同步器状态,AQS是一个依赖状态(state)的同步器。默认为0表示当前没有线程持有锁,
private volatile int state;
模型图示
ReentrantLock执行逻辑
实例化
ReentrantLock有两个构造函数:
//1.有参构造函数,false为非公平锁,true为公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//2.无参构造函数,非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
加锁
lock
当使用lock.lock()去获取锁的时候,代码执行逻辑:
//ReentrantLock
public void lock() {
sync.lock();
}
//ReentrantLock.Sync
//抽象方法,有两个实现分别是NonfairSync、FairSync
abstract void lock();
//ReentrantLock.FairSync
final void lock() {
acquire(1);
}
acquire
- tryAcquire,竞争锁的行为,由子类实现
- addWaiter,竞争锁失败的线程进入同步等待队列;acquireQueued,对成功入队的线程进行阻塞
//AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//acquireQueued返回true说明线程被中断,但是中断标记被清除了
//线程自我中断,打上中断标记,为了能让外部感知到线程中断
selfInterrupt();
}
*tryAcquire
竞争锁,公平锁实现逻辑:
- 获取当前同步器的状态state并判断是否为0
- state = 0可以加锁,state通过CAS修改为1
- state != 0且当前持有线程是自己,state再+1
- state != 0且当前持有线程不是自己,竞争锁失败
//ReentrantLock.FairSync
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前同步器的状态,并判断是否为0
int c = getState();
//state = 0,当前同步器还没有被任何线程持有,可以加锁
//hasQueuedPredecessors判断前面是否有线程排队(公平锁)
//compareAndSetState通过CAS获取锁,修改state为1
//setExclusiveOwnerThread获取到锁之后设置自己为当前持有线程
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//state != 0,表示当前已有线程持有锁,判断当前持有线程是不是自己
//如果是自己,state再+1。因为该分支永远只会有一个线程进入,所以不需要CAS修改state
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//state != 0,而且当前持有线程不是自己,那么加锁失败
return false;
}
*addWaiter
对竞争锁失败的线程进行入队操作:
- 创建节点node,引用当前需要入队线程,模式为独占EXCLUSIVE;
- 如果还没有队列,就先初始化队列,将头尾节点指向空节点;
- 如果有队列,就将node通过CAS的方式插入到尾节点,整个过程会不断循环直到插入队列成功。
//AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {
//传入当前线程的引用作为Node的构造参数
//模式是Node.EXCLUSIVE,独占
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//入队
enq(node);
return node;
}
//AbstractQueuedSynchronizer
private Node enq(final Node node) {
//自旋,使入队失败的线程不断重试保证入队成功
for (;;) {
Node t = tail;
//初始化队列
if (t == null) {
if (compareAndSetHead(new Node()))
//将头尾节点设置为空节点
tail = head;
//原子插入队列,将当前节点设置为尾节点
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
线程入队示意图:
*acquireQueued
对入队的线程再次尝试获取锁或进行阻塞:
- 先再次尝试获取锁,尽可能避免阻塞线程;
- 再次获取成功,出队操作;
- 将当前节点设置为空节点并设置为头节点;
- 原头节点不再被引用被GC回收;
- 再次获取失败,则对线程进行阻塞操作;
- 把头节点状态改为-1,表示当前线程可唤醒;
- 将当前线程阻塞;
- 再次获取成功,出队操作;
//AbstractQueuedSynchronizer
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)) {
//获取成功,出队,将当前节点的thread引用和prev置为null,并设置当前节点为头节点
setHead(node);
//将原头节点的next置为null,此时该节点没有被引用,会被GC回收
p.next = null;
failed = false;
return interrupted;
}
//再次获取失败,对线程进行阻塞操作
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//标记线程被中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前置节点pred的状态
int ws = pred.waitStatus;
//如果前置节点pred状态为SIGNAL(-1),那么当前节点node可以被唤醒
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//把前置节点pred的状态改为SIGNAL(-1),表示可以唤醒当前节点node
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
//通过LockSupport阻塞线程,直到被唤醒(两种方式:1.手动unpark();2.线程中断interrupt())
LockSupport.park(this);
//返回线程中断状态,并清除掉线程中断信号
//如果是中断唤醒,这里会返回true
return Thread.interrupted();
}
线程出队示意图:
线程阻塞示意图:
释放锁
unlock
当使用lock.unlock()去释放锁的时候,代码执行逻辑:
- tryRelease方法,释放锁;
- unparkSuccessor方法,唤醒线程,需先判断h.waitStatus != 0,否则无法唤醒;
//ReentrantLock
public void unlock() {
sync.release(1);
}
//AbstractQueuedSynchronizer
public final boolean release(int arg) {
//释放锁成功
if (tryRelease(arg)) {
Node h = head;
//判断头节点不为null且状态不为0,状态为0则无法唤醒
if (h != null && h.waitStatus != 0)
//唤醒线程
unparkSuccessor(h);
return true;
}
return false;
}
*tryRelease
释放锁操作:
- 每释放一次锁就将state-1,直到state=0;
- 当前持有锁的线程引用置为null;
//ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
//状态state-1,直到减为0
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//当前持有锁的线程设置为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
*unparkSuccessor
唤醒线程操作:
- 头节点状态修改为0;
- 唤醒线程;
//AbstractQueuedSynchronizer
private void unparkSuccessor(Node node) {
//获取头节点状态
int ws = node.waitStatus;
if (ws < 0)
//头节点状态改回0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒头节点的后置节点所引用线程
LockSupport.unpark(s.thread);
}
线程唤醒示意图:
当线程被唤醒之后,会在acquireQueued方法中再次开启一轮循环,重新tryAcquire尝试获取锁。
- 如果是在公平锁的情况下,当前node作为最前面的节点一定会获取到锁,因为没人和他竞争;
- 但如果是非公平锁的情况下,如果此时又来了一个新的线程去竞争锁(可能会成功),那么就不能保证当前node引用的线程一定能获取成功,如果不成功就会再次走shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()逻辑,重新执行两轮循环(线程阻塞示意图)修改头节点状态为-1并阻塞当前线程。
waitStatus状态变化
CANCELLED = 1; SIGNAL = -1; CONDITION = -2; PROPAGATE = -3; 初始值 = 0;
- waitStatus初始值为0;
- 当要阻塞线程时shouldParkAfterFailedAcquire会把头节点的waitStatus修改为-1(0 —> -1),然后parkAndCheckInterrupt阻塞线程,是因为在释放锁的时候,release会判断h.waitStatus != 0,状态为0则无法唤醒;
- 在释放锁的时候会唤醒线程unparkSuccessor,会把头节点的waitStatus改回初始值0(-1 —> 0);
- 线程唤醒之后会再一次tryAcquire尝试获取锁,如果获取失败(非公平锁情况)还会重新走2的逻辑,修改头节点的waitStatus为-1(0 —> -1)并阻塞;
lockInterruptibly
ReentrantLock除了有lock方法加锁,还有lockInterruptibly也可以加锁,他们的区别在于:
- lock方法检测到线程中断依然会继续获取锁,直到获取锁成功后才会把当前线程打上中断标记;
- lockInterruptibly方法遇到线程中断时,会抛出InterruptedException直接返回,不会再获取锁。
lockInterruptibly源代码:
//ReentrantLock
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//AbstractQueuedSynchronizer
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果线程中断则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
doAcquireInterruptibly执行逻辑与acquireQueued的逻辑很类似,不同点在于if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())逻辑,acquireQueued会标记interrupted = true;而doAcquireInterruptibly是直接抛异常InterruptedException。
//AbstractQueuedSynchronizer
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;
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//抛异常,先进入finally代码块,failed = true
throw new InterruptedException();
}
} finally {
if (failed)
//标记当前节点状态为CANCELLED = 1,出现异常,需要废弃结束节点
cancelAcquire(node);
}
}
cancelAcquire
废弃节点的处理逻辑,总体来说就两部分:
- 将节点状态置为CANCELLED=1;
- 将节点所有的引用以及被引用(prev、next、thread等)都置为null,以便节点被GC回收;
private void cancelAcquire(Node node) {
if (node == null)
return;
//将节点引用线程置为null
node.thread = null;
//找出所有需要废弃的cancel节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
//将节点状态置为CANCELLED
node.waitStatus = Node.CANCELLED;
//下面逻辑就是将废弃节点的指针指向置为null,以及指向当前节点的指针修改
//如果当前节点是尾节点的处理逻辑
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
//如果当前节点是头节点的处理逻辑
} else {
unparkSuccessor(node);
}
node.next = node;
}
}