Condition介绍
Condition的作用是对锁进行更精确的控制,使锁的粒度更细小。Condition中的await()方法相当于Object中的wait()方法,Condition中的signal()方法相当于Object中的notify()方法,Condition中的signalAll()相当于Object中的notifyAll()方法。不同的是,Object中的wait()、notify()、notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的,而Condition是与"互斥锁"或"共享锁"捆绑使用的。
Condition方法列表
// 造成当前线程在接到信号或被中断之前一直处于等待状态
void await()
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
boolean await(long time, TimeUnit unit)
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
long awaitNanos(long nanosTimeout)
// 造成当前线程在接到信号之前一直处于等待状态
void awaitUninterruptibly()
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
boolean awaitUntil(Date deadline)
// 唤醒一个等待线程
void signal()
// 唤醒所有等待线程
void signalAll()
Condition示例
示例1:
public class WaitTest1 {
public static void main(String args[]) {
ThreadA ta = new ThreadA("ta");
synchronized (ta) {
try {
System.out.println(Thread.currentThread().getName() + " start ta.");
ta.start();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " block.");
ta.wait();
System.out.println(Thread.currentThread().getName() + " continue.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ThreadA extends Thread {
public ThreadA(String name) {
super(name);
}
public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " wake up others.");
notify();
}
}
}
}
// 结果
main start ta.
main block.//1秒后打印
ta wake up others.
main continue.
分析:
①主线程和子线程使用的锁是同一个对象,都是ta
②先进入主线程的main方法的同步代码块,即主线程先获取到锁对象,打印出:main start ta.
③紧接着,在主线程中开启子线程,但是此时主线程没有释放锁,子线程同步代码块中的代码得不到执行,1秒钟之后主线程打印出:main block.
④紧接着主线程中调用ta.wait();释放锁,主线程进入阻塞状态
⑤子线程等待的锁得到释放,子线程可以获取到锁了,子线程获取到锁之后,打印出:ta wake up others.
⑥紧接着子线程调用了notify(),其实是this.notify(),也就是会唤醒持有当前对象作为锁的线程,而主线程恰好正是持有该对象作为锁的阻塞线程,即主线程被唤醒,接着打印出:main continue.
示例2:
public class ConditionTest1 {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String args[]) {
ThreadA ta = new ThreadA("ta");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " start ta.");
ta.start();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " block.");
condition.await();
System.out.println(Thread.currentThread().getName() + " continue.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
static class ThreadA extends Thread {
public ThreadA(String name) {
super(name);
}
public void run() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " wake up others.");
condition.signal();
} finally {
lock.unlock();
}
}
}
}
// 结果
main start ta.
main block.
ta wake up others.
main continue.
分析:示例2和示例1的流程大致相同,只不过示例1使用的是对象的同步锁而示例2使用的是ReentrantLock,通过Condition实现对持有ReentrantLock锁的线程进行阻塞和唤醒
示例1与示例2对比
:
Object Condition
阻塞 wait await
唤醒某线程 notify signal
唤醒所有线程 notifyAll signalAll
但是,Condition除了支持上面的功能之外,它更强大的地方在于:能够更加精细的控制多线程的阻塞与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,“读线程"需要等待。如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。使用哪个Condition对象阻塞线程就使用哪个Conditiond对象唤醒该线程。看看下面的示例3,可能对这个概念有更深刻的理解:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
// 缓冲区容量:5
final Object[] items = new Object[5];
int putptr, takeptr, count;// count记录实际容量
// 存放
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();// 满了则使当前持有lock锁的存放线程等待,可理解为此时存放线程被notFull这个Condition对象阻塞
items[putptr] = x;// 在下一个位置存放对象x
if (++putptr == items.length)// 使putptr+1,保证下次存放时该位置不被下一个对象覆盖
putptr = 0;// 若已存满则置为0,防止下标越界
++count;
notEmpty.signal();// 唤醒使用notEmpty这个Condition对象阻塞的线程,即取用线程
System.out.println(Thread.currentThread().getName() + " put " + (Integer) x);
} 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();
System.out.println(Thread.currentThread().getName() + " take " + (Integer) x);
return x;
} finally {
lock.unlock();
}
}
}
public class ConditionTest2 {
private static BoundedBuffer bb = new BoundedBuffer();
public static void main(String args[]) {
// 启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9)
// 启动10个“读线程”,从BoundedBuffer中不断的读数据
for (int i = 0; i < 10; i++) {
new PutThread("p" + i, i).start();
new TakeThread("t" + i).start();
}
}
static class PutThread extends Thread {
private int num;
public PutThread(String name, int num) {
super(name);
this.num = num;
}
public void run() {
try {
Thread.sleep(1); // 线程休眠1ms
bb.put(num); // 向BoundedBuffer中写入数据
} catch (InterruptedException e) {
}
}
}
static class TakeThread extends Thread {
public TakeThread(String name) {
super(name);
}
public void run() {
try {
Thread.sleep(10); // 线程休眠1ms
bb.take(); // 从BoundedBuffer中取出数据
} catch (InterruptedException e) {
}
}
}
}
// 结果
p2 put 2
p0 put 0
p7 put 7
p5 put 5
p4 put 4
t0 take 2
p3 put 3
t4 take 0
p1 put 1
t3 take 7
p8 put 8
t1 take 5
p9 put 9
t2 take 4
p6 put 6
t7 take 3
t8 take 1
t9 take 8
t5 take 9
t6 take 6
说明:
①BoundedBuffer 是容量为5的缓冲区,缓冲区中存储的是Object对象,支持多线程的读/写。多个线程操作“一个BoundedBuffer对象”时,它们通过互斥锁lock对缓冲区进行互斥访问;而且同一个BoundedBuffer对象下的全部线程共用“notFull”和“notEmpty”这两个Condition。notFull用于控制写缓冲,notEmpty用于控制读缓冲。当缓冲已满的时候,调用put的线程会执行notFull.await()进行等待;当缓冲区不是满的状态时,就将对象添加到缓冲区并将缓冲区的容量count+1。最后,调用notEmpty.signal()唤醒notEmpty上的等待线程(调用notEmpty.await的线程)。 简言之,notFull控制“缓冲区的写入”,当往缓冲区写入数据之后会唤醒notEmpty上的等待线程;同理,notEmpty控制“缓冲区的读取”,当读取了缓冲区数据之后会唤醒notFull上的等待线程。
②在ConditionTest2的main函数中,启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9);同时,也启动10个“读线程”,从BoundedBuffer中不断的读数据。
分析:
首先,打印出的结果是不确定的,因为这20个线程的执行存在一定的随机性。在for循环中不断的开启新的线程,这些线程在调用put()或take()时又是使用的同一个锁——lock,那么在一个时间片内只能有一个线程的put()或take()方法得以执行,在该线程的put()或take()方法释放锁之前其他线程都会被阻塞。开始时,缓冲区不满,锁是通过put()方法执行结束在finally块中释放的,随着缓冲区的实际容量增加,缓冲区可能会满,这时就会通过condition的await()方法释放锁;take()方法也是如此。在put()中使用await()释放锁之后下一个获取锁的线程可能还是一个put线程,这样就导致有多个put线程因为await()而阻塞,最多会有5个被阻塞的线程。
疑问:在示例3中最多会有5个线程被阻塞,为什么不用signalAll()而使用signal()唤醒线程呢?
我的理解是使用signalAll()和signal()都可以,达到的效果是一样的。因为阻塞的线程使用的是同一把锁,即使使用signalAll()将所有的线程都唤醒也只会使其中的一个线程运行。果真如此的话,那使用同一个condition的await()阻塞的线程使用的锁必然是同一把锁,在使用condition的唤醒方法唤醒这些阻塞线程后又只会使其中的一个线程运行,那么当这些线程执行的代码相同的时候唤醒所有的线程也就没有必要了,此时使用signal()去唤醒即可。那什么情况下使用signalAll()呢?应该是在使用同一个condition对象的await()阻塞的线程,但是这些线程执行的是不同的代码(线程执行不同的任务),而且这些线程的执行顺序没有要求的时候,可以使用signalAll()去唤醒这些线程。
最重要的是:使用Condition是可以具体指明阻塞和唤醒哪个线程的。
简单点理解的话:Lock相当于家里大门的锁,而Condition相当于家里每个房间小门的锁。