ReentrantLock类有一个方法newCondition用来生成这个锁对象的一个条件(ConditionObject)对象,它实现了Condition接口。Condition提供了线程通讯的一套机制await和signal等线程间进行通讯的方法。。
1、适用场景
当某线程获取了锁对象,但因为某些条件没有满足,需要在这个条件上等待,直到条件满足才能够往下继续执行时,就需要用到条件锁。
这种情况下,线程主动在某条件上阻塞,当其它线程发现条件发生变化时,就可以唤醒阻塞在此条件上的线程。
2、使用示例
下面是来自JDK的一段示例代码,需要先获得某个锁对象之后,才能调用这个锁的条件对象进行阻塞。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
注意上面的代码,先是通过lock.lock获得了锁对象,然后发现条件不满足时(count==items.length),缓存已满,无法继续往里面写入数据,这时候就调用条件对象notFull.await()进行阻塞。
如果条件满足,就会往缓存中写入数据,同时通知等待缓存非空的线程,notEmpty.signal.
这样就实现了读线程和写线程之间的通讯
3、线程阻塞对锁的影响
上面的例子中,线程是先获得了锁对象之后,然后调用notFull.await进行的线程阻塞。在这种情况下,拥有锁的线程进入阻塞,是否可能会造成死锁。
答案当然是否定的。因为线程在调用条件对象的await方法中,首先会释放当前的锁,然后才让自己进入阻塞状态,等待唤醒。
4、线程的条件等待、唤醒与锁对象的关系
在ReentrantLock解析中说过,
AbstractQueuedSynchronizer的内部维护了一个队列,等待该锁的线程是在这个队列中。类似的,ConditionObject内部也是维护了一个队列,等待该条件的线程也构成了一个队列。
当现成调用await进入阻塞时,便会加入到ConditionObject内部的等待队列中。注意,这里是自己主动进入阻塞,除非被其它线程唤醒或者被中断,否则线程将一直阻塞下去。
当其它线程调用signal唤醒阻塞的线程时,便把等待队列中的第一个节点从队列中移除,同时把节点加入到
AbstractQueuedSynchronizer 锁对象内的等待队列中。为什么是进入到锁的等待队列中?因为线程被唤醒之后,并不意味着就能立刻执行。此时,其它线程有可能正好拥有这个锁,前面也已经有现成在等待这个锁,所以被唤醒的线程需要进入锁的等待队列中,在前面的线程执行完成后,才能继续后续的操作。
可参考下图
5、线程是否能同时处于条件对象的等待队列中和锁对象的等待队列中
不能。线程只有调用条件对象的await方法,才能进入这个条件对象的等待队列中。而线程在调用await方法的前提是线程已经获取了锁,所以线程是在拥有锁的状态下进入条件对象的等待队列的,拥有锁的线程也就是正在运行的线程,是不在锁对象的等待队列中的。
只有当一个线程试着获取锁的时候,而这个锁正好又由其它线程占据的时候,线程才会进入锁的等待队列中,等待拥有锁的线程执行完成,释放锁的时候被唤醒。
6、实现原理
相关代码在
AbstractQueuedSynchronizer的内部类ConditionObject中可以看到。
ConditionObject有两个属性firstWaiter和lastWaiter,分别指向的是这个条件对象等待队列的头和尾。
队列的各个节点都是Node(
AbstractQueuedSynchronizer的内部类
)对象,通过Node对象的nextWaiter之间进行向下传递,所以,条件对象的等待队列是一个单向链表。
下面是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
)
unlinkCancelledWaiters();
if
(interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
首先是调用addConditionWaiter把当前线程加入到条件对象的等待队列中,然后fullyRelease来释放锁,然后通过isOnSyncQueue来检查当前线程节点是否在锁对象的等待队列中。
为什么要做这个检查?因为线程被signal唤醒的时候,是首先加入到锁对象的等待队列中的。如果没有在锁对象的等待队列中,那么说明事件还没有发生(也就是没有signal方法没有被调用),所以线程需要阻塞来等待被唤醒。
在addConditionWaiter方法中完成了等待队列的构建过程,代码如下
private
Node addConditionWaiter() {
Node t =
lastWaiter
;
// If lastWaiter is cancelled, clean out.
if
(t !=
null
&& t.
waitStatus
!= Node.
CONDITION
) {
unlinkCancelledWaiters();
t =
lastWaiter
;
}
Node node =
new
Node(Thread.currentThread(), Node.
CONDITION
);
if
(t ==
null
)
firstWaiter
= node;
else
t.
nextWaiter
= node;
lastWaiter
= node;
return
node;
}
线程加入队列的顺序与加入的时间一致,刚加入的线程是在队列的最后面。
下面来看线程的唤醒
public
final
void
signal() {
if
(!isHeldExclusively())
throw
new
IllegalMonitorStateException();
Node first =
firstWaiter
;
if
(first !=
null
)
doSignal(first);
}
唤醒操作实际上是通过doSignal完成,注意这里传递的是firstWaiter指向的节点,也就是唤醒的时候,是从队列头开始唤醒的。
从尾部进入,从头部唤醒,所以这里的等待队列是一个FIFO队列。
private
void
doSignal (Node first) {
do
{
if
( (
firstWaiter
= first.
nextWaiter
) ==
null
)
lastWaiter
=
null
;
first.
nextWaiter
=
null
;
}
while
(!transferForSignal(first) &&
(first =
firstWaiter
) !=
null
);
}
doSignal方法把第一个节点从条件对象的等待队列中移除,然后
最终是走到transferForSignal中来进行操作。
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
;
}
通过enq方法,把线程所在的节点加入到锁对象的等待队列中,这样在条件合适的时候,线程被唤醒,获得锁,然后执行。