LockSupport 与 Condition
LockSupport
定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组建的基础工具.
LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark方法来唤醒一个被阻塞的线程.这些方法及描述如下表所示:
jdk1.6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3个方法,用于阻塞当前线程.其中blocker主要用来标识当前线程在等待的对象(也就是阻塞对象).–主要是为了方便问题排查和系统监控.
Condition
任何Java对象,都有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()和notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式.Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式.
不过这两者在使用方式以及功能特性上还是有差别的,具体看下表对比
Condition接口与示例
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁.Condition对象是由Lock对象创建出来的,换句话说,Condition时依赖Lock对象的.
使用方式如下所示:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException{
lock.lock();
try{
condition.await();
}finally{
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException{
lock.lock();
try{
condition.signal();
}finally{
lock.unlock();
}
}
当调用await()方法后,一般会将当前线程释放锁并在此等待,而其他线程调用Condition对象的signal(),通知当前线程后,当前线程才从await()方法返回,并且在返回前就已经获取了锁.
Condition的实现分析
ConditionObject是同步器AQS的内部类,因为Condition的操作需要获取到相关的锁.每个Condition对象都包含一个队列-等待队列,该队列是Condition实现等待通知的关键.
1、等待队列
等待队列是一个FIFO的队列,队列中的每个节点都包含了一个线程的引用,该线程就是Condition对象上等待的线程,如果一个线程调用了Condtion.await()方法,那么该线程将会释放锁、构造节点,并加入等待队列并进入等待状态.
这个和同步器的节点定义是一样的,也就是说,同步队列和等待队列中的节点类型用的都是同步器的静态内部类.
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾结点(lastWaiter).当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列基本结构如图所示
Condition拥有收尾节点的使用,而新增节点只需要将原有的尾结点的nextWaiter指向他,并切更新尾节点即可.这个节点更新的过程并没有使用CAS来保证,因为调用await()方法的线程必定已经获取到锁了,也就是该过程是由锁来保证线程安全的.
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列,其关系如下所示:
Condition的实现是同步器的内部类,因此每个Condition实例都能访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用.
2、等待
调用Condition的await(),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态.当从await()方法返回时,当前线程一定获取了Condition相关的锁.
如果从队列的角度看(等待队列和同步队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到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;
}
// 退出while循环后,加入到获取同步状态的竞争中
if(acquireQueued(node,savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if(node.nextWaiter != null)
unlinkCancelledWaiters();
if(interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态.
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态.如果不是通过其他线程调用Contition.signal()方法唤醒的,而是通过对等待线程进行终端,则会抛出InterruptedException.
如果是从队列的角度看,当前线程加入Condition的等待队列并不会直接加入,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列.
3、通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中.
代码如下:
public final void signal(){
// 当前线程必须是获取了锁的线程
if(!isHeldExclusive())
throw new IllegalMonitorStateException();
// 获取等待队列的首节点
Node first = firstWaiter;
if(first != null)
// 将其移动到同步队列,并使用LockSupport唤醒节点中的线程.
doSignal(first);
}
节点从等待队列移动到同步队列的过程
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中.
成功获取同步状态之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功的获取了锁.
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有的节点全部移动到同步队列中,并唤醒每个节点的线程.
总结
jdk既然提供了monitor对象和synchronized关键字配合wait、notify、notifyAll方法实现等待/通知模式,为什么还要提供Lock和Condition配合await、signal、signalAll来实现等待/通知模式
- Lock可以显性的获取和释放锁,synchronized是隐性的.
- Lock支持手动获取锁,释放锁,我们可以自定义我们需要的锁,比如可重入锁,读写锁,具备更强的可拓展性.
- condition支持多个等待队列即多个条件变量或者锁投票,支持可中断等候,支持时间锁等候、无块结构锁.