AQS的队列图
ReentrantLock是java.util.concurrent包提供的重入锁,其同步操作由AQS同步器提供支持。ReentrantLock提供了一些其他功能,包括定时的锁等待,可中断的锁等待,公平锁,非公平锁等。
ReentrantLock的独占并可重入:
新建一个ReentrantLock的时候可以通过传参true和flase来创建公平锁和非公平锁。
//默认就是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
fair参数如果不传,就默认创建一个非公平锁。
调用lock方法的时候,会到公平锁或者非公平锁的同步器进行不同的处理。
//实现非公平锁的同步器
static final class NonfairSync extends Sync {
final void lock() {
...
}
}
//实现公平锁的同步器
static final class FairSync extends Sync {
final void lock() {
...
}
}
我们先看默认的非公平锁是怎么调用lock加锁的。
final void lock() {
if (compareAndSetState(0, 1))
//将当前线程赋值为独占锁
setExclusiveOwnerThread(Thread.currentThread());
else
//否则表明锁已经被占用, 调用acquire让线程去同步队列排队获取
acquire(1);
}
进入lock方法,会用CAS尝试进行加锁,也就是将同步的状态值state从0更新成1.如果更新成功就是加锁成功。将当前线程赋值为独占模式的持有者。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面,是下面的acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法可以分成三步看。
第一步!tryAcquire(arg)是去尝试获取锁。
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//获取state变量值 0是没有线程获取锁 1是已经有线程获取到锁
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");
// 更新state值为新的重入次数
setState(nextc);
return true;
}
//获取锁失败
return false;
}
非公平锁tryAcquire的流程是:判断state是否等于0,如果等于0说明没有线程获取到锁,所以尝试加锁,如果state大于0,说明有线程持有锁,再判断当前线程是否是持有锁的线程。如果是持有锁的线程那么更新重入锁的次数,否则获取线程失败。
第二步acquireQueued(addWaiter(Node.EXCLUSIVE), arg))进入队列。假设ABC三个线程抢锁,A获取到锁后,那么B和C会进入队列进行等待。
//mode分为独占和共享模式 Node.EXCLUSIVE独占 Node.SHARED为共享
private Node addWaiter(Node mode) {
//初始化节点,设置关联线程和模式(独占 or 共享)
Node node = new Node(Thread.currentThread(), mode);
// 获取队列的尾节点引用
Node pred = tail;
// 如果尾节点不为null 说明队列初始化过了,直接设置新节点为尾节点
if (pred != null) {
node.prev = pred;
//通过cas进行设置尾节点,防止多线程丢失线程进列的操作
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尾节点为空,先初始化head节点并入队新节点
enq(node);
return node;
}
private Node enq(final Node node) {
//开始自转
for (;;) {
//获取首节点
Node t = tail;
//首节点为空 cas设置首节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//首节点不为空 说明初始化过了,那么就把当前节点设置为尾节点。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
假如ABC三个线程抢锁,A获取到锁后,那么B和C会同时进入队列进行,进入addWaiter方法时,队列未初始化,假如BC同时进入enq进行首节点的cas的设置,第一轮BC通过cas抢夺首节点的设置,假如B成功了,那么C进行第二轮抢夺,这时首节点不为空了,那么C就抢夺尾节点的cas设置了。
//进入队列的节点再次尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
//是否成功获取锁
boolean failed = true;
try {
//线程是否被中断过
boolean interrupted = false;
for (;;) {
//获取前驱节点
final Node p = node.predecessor();
//如果前驱节点是head 并且尝试加锁成功
if (p == head && tryAcquire(arg)) {
//加锁成功,将当前节点设置为head节点
setHead(node);
//清空原来头结点的引用(next)便于GC
p.next = null;
//获取成功
failed = false;
//返回中断标识 退出自旋
return interrupted;
}
//获取失败后是否挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//最终都没能获取同步状态,结束该线程的请求
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态
int ws = pred.waitStatus;
//判断是否要阻塞当前线程
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//状态>0 遍历前驱结点直到找到不是结束状态的结点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的状态设置为SIGNAL状态
//确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。 毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//只有当前驱节点是唤醒状态,才挂起当前结点
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
注意:只有在前驱结点已经是SIGNAL状态后才会执行后面的方法立即阻塞,对应上面的第一种情况。其他两种情况则因为返回false而重新执行一遍。
释放锁:
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) {
//获取state状态-1的数值
int c = getState() - releases;
//判断当前线程是否是持有锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果是0 说明可以释放锁
if (c == 0) {
free = true;
//将持有锁的线程赋值null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
//获取节点的状态
int ws = node.waitStatus;
//如果小于0 就设置为0 就是初始化无状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//找到当前节点的下一个节点
Node s = node.next;
//如果下个节点是null或者状态>0 (节点因超时或被中断而取消时设置状态为取消状态)
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);
}
公平锁:
公平锁是进来直接调用acquire(1)方法,非公平锁是进来直接抢锁。
final void lock() {
acquire(1);
}
下面是非公平的加锁解锁过程图
总结:
ReentrantLock本身也是一种支持重进入的锁:即该锁可以支持一个线程对共享资源重复加锁;
ReentrantLock公平锁:只尝试抢锁一次(进入acquire(1)判断state==0的时候抢锁),入列后等待解锁后的线程进行唤醒。
ReentrantLock非公平锁:抢锁两次,进来后先尝试抢锁,如果没有抢到在进入acquire(1)判断state==0的时候再次抢锁,进入队列后也只能等待解锁后的线程进行唤醒。
使用场景:
如果发现该操作已经在执行中则不再执行,多用于定时任务,一个资源只有一个操作。
if (lock.tryLock()) { //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
try {
//操作
} finally {
lock.unlock();
}
}
如果发现该操作已经在执行,等待一个一个执行。比如同时写一个文件,不同情景同时修改一条信息。
try {
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
//操作
} finally {
lock.unlock();
}
如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行。使用超时机制。
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException
}
如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。
取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞。
try {
lock.lockInterruptibly();
//操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}