1.定义
在操作系统——多线程同步互斥一文中已详细介绍过了条件变量及其使用注意事项。条件变量,是为了解决等待同步需求,实现线程间协作通信的一种机制。条件变量用来自动阻塞一个线程,直到某特殊情况发生唤醒该线程为止。通常条件变量和锁机制同时使用。
2.Java中的条件变量
2.1 synchronized + wait()、notify()、notifyAll()
线程A(消费者):
synchronized(obj){
while(cond is false){
obj.wait(); //不满足条件,加入等待队列,自动释放锁
}
consume();
alter cond;
}
线程B(生产者):
synchronized(obj){
produce();
alter cond;
if(cond is true){
obj.notify();
}
}
为什么wait()和notify()必须放在synchronized里面?
其实这已经在“操作系统——多线程同步互斥” 条件变量那一块说过。
如果A在判断part不满足条件后,此时可能由于时间片轮换造成消费者还没来得及执行wait()操作,导致wait队列中并不存在此线程,恰巧在此时切换到了某个生产者线程中,并且恰好满足了条件,发出了cond信号,此时os便在wait队列中查找等待中的线程,但是发现并没有等待线程,所以这个cond信号便丢失了,造成后面切换到消费者线程,继续执行到wait()时,已经错过了这个信号,导致消费者丢失了此次信号,处于阻塞态。
正确做法是当线程切换导致消费者来不及进入wait队列时,其它程序也不可以操作该共享变量,既保证消费者wait()和条件判断是一个原子操作,而最常见的做法便是在条件判断到wait()的这一段代码加一个互斥锁进行保护,另外生产者操作共享资源时,必须先获得锁才行。
2.2 重入锁+Condition对象(await()、signal()、signalAll())
在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。 这样看来,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建立不同的Condition。
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;//如果写索引写到队列的最后一个位置了,那么置为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;//如果读索引读到队列的最后一个位置了,那么置为0
--count;//个数--
notFull.signal();//唤醒写线程
return x;
} finally {
lock.unlock();
}
}
}
我们可以看到Condition这种方式是和重入锁搭配使用的,与前一种不同之处在于,前一种的synchronized块会在一开始就加锁,结束时解锁,不需要显示加锁,解锁;而后一种重入锁需要显示的加锁和解锁。
而这正是重入锁和synchronized的最大区别,所以造成了synchronized太过粗糙,无法做到锁的细粒度控制,而重入锁可以更细粒度更灵活的控制程序。
3.Java中的另一种协作机制
JDK提供了join()、yield()两个线程间的协作函数。
-
t.join() 方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING 状态,直到线程t完成,才会发出 t.notifyAll() 通知让所有等待线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
从上面可看出,t.join() 方法会默认加锁和解锁(因为其本质是通过 wait() 和 notifyAll() 实现的),由于t.notifyAll() 会通知所有等待线程继续执行,所以不要在Thread对象实例上使用wait()、notify()等方法,因为可能会影响系统api或被其影响。public class TestJoin { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new JoinThread());; Thread thread2 = new Thread(new JoinThread()); thread1.start(); thread1.join();//线程1加入,表示我要执行完,其余线程(线程2)才可以继续执行 thread2.start(); } static class JoinThread implements Runnable { @Override public void run() { for(int i=0; i<100; i++) { try { System.out.println(Thread.currentThread().getName()); Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
-
yield()方法恰好与join()相反,表示我可以让出cpu等待其余线程先执行,但是就算让出了,其还是会争夺cpu资源的,只是不一定被分配到而已。
所以如果某个线程优先级低,不那么重要,而且害怕它会占用太多cpu时间,就可以调用Thread.yiled()——静态方法.