ReentrantLock
ReentrantLock的加锁和解锁
ReentrantLock是juc以api的形式提供的可重入锁,典型的使用方式是
reentrantLock.lock();
doSomeThing();
reentrantLock.unlock();
lock表示加锁,unlock表示解锁,接下来我们就看看是怎么实现的
lock
public void lock() {
sync.lock();
}
在ReentrantLock里面,lock有两种实现,一种是公平锁FairSync、一种是非公平锁NonfairSync
下面是NonfairSync的实现:首先使用cas修改state的值,如果修改成功,说明可以拿到锁,设置当前线程是获取到锁的线程。如果修改失败,则用排它模式获取。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
acquire会进到aqs里的acquire方法。
这里的代码会先尝试获取锁(tryAcquire),如果获取失败,则将当前线程加到队列里(addWaiter),并将当前线程挂起(acquireQueued)。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
再来看下tryAcquire的实现。
tryAcquire最终会进到NonfairSync的nonfairTryAcquire的方法。这个方法线判断当前state是否是0,如果是0,说明可以获取资源,那就用cas修改state的值,如果修改成功,则说明当前线程获取到了锁,设置当前线程为获取锁的线程。如果state不是0,说明已经被枷锁,就判断当前线程是不是获取锁的线程,如果是,就加获取数量加1。如果没有获取到锁,或者获取锁的线程不是当前线程,则返回false。
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;
}
上面我们说过Sync在ReentrantLock里有2种实现,公平和非公平其实就体现在获取资源方面
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;
}
}
nofairTryAcquire和tryAcquire的区别在于,如果新加的线程发现当前资源可以获取时,非公平实现方式直接尝试获取,公平实现方式会线判断当前队列是否有等待的线程,如果没有才获取。
addWait方法会将当前线程加入到队列里面。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
然后就是acquireQueued方法。
这个方法会在循环里面获取当前节点的前一个节点。如果前一个节点是头节点,当前线程会尝试再获取一次资源,如果获取到,就将当前节点设置为头节点。如果前一个节点不是头节点,或者没有获取到资源,则将当前线程挂起。
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);
}
}
shouldParkAfterFailedAcquire判断当前节点前面节点的状态,
- 如果是SIGNAL,就说明当前节点可以park,前面节点会unpark当前节点的线程
- 如果是>0,说明前面节点已经取消,是无用节点,删除这些节点,当前节点不能park,因为可能没有节点unpark当前节点的线程
- 其它情况下,将前面节点置为SIGNAL,当前节点不能park,因为可能没有节点unpark当前节点的线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
unlock
首先是unlock方法,调用sync的release方法,释放1个资源
public void unlock() {
sync.release(1);
}
在release方法,先tryRelease释放资源,判断当前线程是不是获取资源的线程,如果不是就报错,如果是就将state的值减去releases的值,如果减去之后=0,就说明资源被全部释放,当前线程不再占有锁了。这时会调用unparkSuccessor将后置节点unpark。
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) {
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;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
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)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
什么是AQS
AQS全称AbstractQueuedSynchronizer,juc里很多工具类都是基于这个实现的。从上面我们可以看出,AQS提供了获取资源、释放资源的能力,如果获取资源成功,当前线程就会继续执行,如果获取资源失败,当前线程就会被挂起(park)。
AQS维护了1个队列,用于存储那些没能获取资源而被挂起的线程。队列里的每个节点都用AQS的内部类Node表示。
Node
Node是同步队列的节点,封装了对应的线程和节点在队列的状态。
队列里的每个节点都有状态:
- CANCELLED,表示当前节点被取消,当前节点表示的线程不会再争夺锁
- SIGNAL,表示当前节点后面节点需要unpark,我们在前面看到shouldParkAfterFailedAcquire如果发现前面节点是SIGNAL状态,就表示自己肯定会被unpark,所以返回true表示可以park当前线程
- CONDITION,表示当前线程正在等待一个condition,condition用到,后面讲
- PROPAGATE,表示下一个acquireShared应该无条件传播
- 0,默认状态
Condition
Condition有await、singal、singalAll方法,对应Object的wait、notify、notifyAll。
Condition也维护了1个队列,表示在当前这个Condition对象上等待的线程。
await方法现在队列里追加一个等待着节点,然后把当前线程获取的资源全部释放,并记录当前线程释放的资源数,后面重新获取锁的时候要用。然后判断当前节点是否在同步队列,这里指的是Node维护的节点,不是Condition维护的节点。如果不在同步队列,则挂起当前线程。当线程从挂起状态恢复后,当前节点已经在同步队列了,这时会尝试获取资源。获取资源之后,把取消的等待着清理掉。
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);
}
signal方法获取第1个等待着,把第1个等待者唤醒。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal方法,修改节点的状态,然后把节点置入同步队列,前段await流程里面会判断是否在同步队列里面,就是这个时候置入同步队列的。
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;}
总结
最开始看AQS没有结合使用场景,不太好理解。从ReentrantLock入手,看AQS如何控制线程的阻塞和唤醒,就比较好理解了。然后引入了AQS的节点的实现、AQS的Condition的实现。
最后还有一个点前面没说道,我们经常看到park、unpark操作,有什么作用呢。park会将当前线程挂起。unpark会将指定线程唤醒,如果指定线程还没有挂起,那么指定线程挂起(park)的事后会直接返回,不会挂起。