谈ReentrantLock之前,我们先来看看AbstractQueuedSynchronizer(AQS)。抽象的队列式同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock、Semaphore、CountDownLatch。
AQS类的详情
状态位state
从源码可以看出状态位state是用volatile修饰的,volatile的作用就是内存可见性,这里就不多说。如果状态位为0,表示当前线程没有获取锁;如果状态位为1的话,则表示当前线程已经获取了锁;当已经获得锁的线程重新获取锁的时候,每获取一次状态位就自增1,这就是可重入锁。
compareAndSetState()这个方法也可以修改状态位的值,如果当前的状态位的值等于期望值,则将同步状态的值进行更新。
还是说一说可重入锁吧。
public class TestReLock {
//是否被锁住
private boolean isLocked = false;
//重入次数
private int count = 0;
//标记得到锁的线程
private Thread getLock = null;
public synchronized void lock() throws InterruptedException {
//当前线程
Thread curThread = Thread.currentThread();
//如果锁已经被其他线程得到
if (isLocked && curThread!=getLock) {
//当前线程进入等待状态
wait();
}
isLocked=true;
count++;
getLock=curThread;
}
public synchronized void unlock() {
Thread curThread = Thread.currentThread();
if (curThread.equals(getLock)) {
count--;
//如果重入次数为0,则是释放掉锁
if (count == 0) {
isLocked=false;
notifyAll();
}
}
}
}
不可重入锁:
public class TestLock {
//是否被锁在
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
if (isLocked) {
wait();
}
isLocked=true;
}
public synchronized void unlock() {
isLocked=false;
notify();
}
}
synchronized是可重入锁,如果不是,下面代码会产生死锁。在调用方法A之前,获得了当前实例对象的锁,然后调用方法B又需要再次获得当前实例对象的锁,可是锁已经被获取了,得不到锁。
public synchronized void methodA() {
methodB();
}
public synchronized void methodB() {
doSomething();
}
CLH队列
Node是AQS的内部类,AQS维护的CLH队列中,每一个节点就是一个Node,代表着需要获取锁的线程。
- SHARE:表示共享锁,多个线程可以获取同一个锁。
- EXCLUSIVE:独占锁,一个锁只能被一个线程所持有。
- CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化.
- SIGNAL:就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
- CONDITION:与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
(摘自:Java并发之AQS详解)
CLH是一个先进先出的线程等待队列,但线程在获取锁失败,被堵塞的时候就会进入此队列。
相关源码
从acquire到release讲
-
acquire(int arg)
这个方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。获取到资源后,线程就可以去执行其临界区代码了。
tryAcquire:尝试去获取锁,若果获取成功则设置锁状态返回true,否则返回false。
addWaiter:将当前获取锁的线程加入到CLH队列列尾。
acquireQueued:让线程在等待队列中等待获取资源,直到获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
selfInterrupt:如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断,将中断补上。
tryAcquire尝试获取锁,addWaiter新建节点并添加到CLH队列中。其中tryAcquire,AQS并没有提供实现,它仅仅只是抛出一个异常,具体的实现需要各个锁自己实现。
private Node addWaiter(Node mode) {
//给定模式构造节点,节点的模式有二种:独占和共享
Node node = new Node(Thread.currentThread(), mode);
// 尝试将节点放到CLH队列列尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//上一步失败则通过enq方法放入队列
enq(node);
return node;
}
private Node enq(final Node node) {
//CAS自旋,直到成功加入队尾
for (;;) {
Node t = tail;
// 如果队列为空,创建一个空的标志头节点head并将其赋给tail
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果队列不为空,则放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
以上就是线程获取锁失败,插入到CLH队列末尾的整个流程。
- acquireQueued(final Node node, int arg)
线程获取锁失败,进入队列尾等待休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。
final boolean acquireQueued(final Node node, int arg) {
//是否成功拿到资源的标记
boolean failed = true;
try {
//标记等待过程是否被中断过
boolean interrupted = false;
//CAS自旋
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点是head,则说明有资格去获取锁,
if (p == head && tryAcquire(arg)) {
//拿到资源后,将head指向该节点
setHead(node);
//setHead中node.prev已置为null,此处再将head.next置为null,
//就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
p.next = null; // help GC
failed = false;
return interrupted;
}
//
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
获取前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到当前节点的前驱状态
int ws = pred.waitStatus;
//SIGNAL标识表示只要前继结点释放锁,就会通知后继结点的线程执行
if (ws == Node.SIGNAL)
return true;
//ws大于0表示CANCELLED
if (ws > 0) {
do {
//前驱节点已经放弃,那就一直往前找
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果找到的前驱正常,那就把前驱的状态设置成SIGNAL,告诉它释放锁后通知自己一下
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
//调用park让线程进入等待状态
LockSupport.park(this);
//如果被唤醒,判断是不是中断唤醒等待中的线程
return Thread.interrupted();
}
acquireQueued主要做了啥?
- 结点进入队尾后,检查状态,找到前驱节点中标识为SIGNAL的接节点
- 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
- 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1.
总结
- 调用acquire方法去获取锁,会调用自定义的tryAcquire()尝试去获取资源,如果成功就直接返回。
- 没有成功,就调用addWaiter()将该线程加入等待队列的尾部,并标记为独占模式。
- acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
释放
release()方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了,即state=0,它会唤醒等待队列里的其他线程来获取资源。
public final boolean release(int arg) {
//根据tryRelease的返回值来判断资源是否释放成功
if (tryRelease(arg)) {
//找到等待队列的头节点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒下一个等待的节点
unparkSuccessor(h);
return true;
}
return false;
}
和tryAcquire方法一样,都是自定义实现。
private void unparkSuccessor(Node node) {
//当前线程所在的节点
int ws = node.waitStatus;
//将当前线程的状态置为0
if (ws < 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)
//<=0的节点都是有效的节点,从后面往前面找
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒节点
LockSupport.unpark(s.thread);
}
共享模式
上面说的都是独占模式下获取锁和释放锁的操作,接下来分析下共享锁的获取和释放。
同样,acquireShared()方法是获取共享资源的顶层入口。获取成功则直接返回,获取失败一样加入到CLH队列末尾进行等待,直到获取资源为止。整个过程也是忽略中断。
tryAcquireShared()需要自定义同步器去实现
private void doAcquireShared(int arg) {
//将共享模式的节点加入队列尾部
final Node node = addWaiter(Node.SHARED);
//是否成功的标志
boolean failed = true;
try {
boolean interrupted = false;
//CAS自旋
for (;;) {
//获取节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点是老大,则当前节点是老二则有资格获取资源
if (p == head) {
//尝试获资源
int r = tryAcquireShared(arg);
if (r >= 0) {
//如果资源还有剩余,可以唤醒之后的线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
//如果被中断过,则将中断补上
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//尝试获取资源失败后,则找到前继节点为signal的节点进行等待
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//head指向自己
setHead(node);
//资源还有剩余量,继续唤醒下一个结点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
共享资源释放
尝试释放资源,唤醒后继节点。
private void doReleaseShared() {
//自旋
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒后继节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
一切都在代码中,就不细说了。博客肯定是先利己,然后再利人。学习很苦,愿学有所成!