第3章主要介绍了数据的同步(Data Synchronization),这一章则主要介绍线程之间的同步方法(Thread Notification),同样包括传统的wait-and-notify方法和JDK 1.5新推出的Condition Variable。在多线程编程中,数据同步和线程同步是两个最基本也是最关键的部分。
《Java Threads》一书中通过考察打字程序中当按下start和stop按钮后,每次都创建两个新的线程的效率问题来引入线程同步的概念,当然不是线程同步的主要用处。不过,教科书归教科书,实际运用则又是另一回事。所以,通过书本学习语法,通过实践来获得运用经验。
4.1 Wait and Notify
1. 等待/唤醒类似于Solaris或POSIX中的条件变量(conditon variables),或者Windows中的事件变量(evant variable)或者信号量(signal),用于某个/多个线程暂停等待某个条件的满足,而该条件将由其它线程来设置的情况。
2. 在Java中,就像每个对象有一个锁之外,任何对象都可以提供等待/唤醒的机制。就像Java中的synchronized总是表示获得某个具体对象的锁一样,wait和notify也总是等待某个具体的对象,并由该对象唤醒;同样,获得某个对象上的锁不一定是该对象需要同步一样,等待和唤醒的条件也不一定是与之绑定的对象。
3. Java中wait-and-notify的几个方法:
void wait(): 使当前线程处于等待状态,直到其它线程调用了nofity()或者notifyAll()方法为止。
void wait(long timeout): 使当前线程处于等待状态,直到其它线程调用了nofity()或者notifyAll()方法,或者超过了指定的时间(单位为ms)为止
void wait(long timeout, int nanos):与wait(long)一样,只是在某些JVM中可以精确到奈秒。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
注意:
任何等待和唤醒方法都必须在与之对应的对象的同步方法或同步块里调用。即:wait-and-notify必须和与之对应的synchronized关键词一起使用的。
4. wait()和sleep()的主要区别:
1) sleep()可以在任何地方调用,而wait()需要在同步方法或同步块中调用;
2) 进入wait()函数时,JVM会自动释放锁,而当从wait()返回即被唤醒时,又会自动获得锁;而sleep()没有这个功能,因此如果在wait()的地方用sleep()代替,则会导致相应的nofity()方法在等待时不可能被触发,因为notify()必须在相应的同步方法或同步块中,而此时这个锁却被sleep()所在的方法占用。也就是说,wait-and-notify不可能与sleep()同时使用。
4.1.1 The Wait-and-Notify Mechanism and Synchronization1. 这一节详细的讲解了wait-and-notify机制和synchronized的关系,主要是两点:1)wait-and-notify必须和synchronized同时使用;2)wait()会自动释放和获取锁;
2. 这一节中举了一个例子用来解释可能存在当条件被不满足时也有可能被唤醒的情况:
1) 线程T1调用一个同步方法;
2) T1检测状态变量,发现其不满足条件;
3) T1调用wait(),并释放锁;
4) 线程T2调用另外一个同步方法,获得锁;
5) 线程T3调用另外一个同步方法,由于T2获得了锁,所以处于等待状态;
6) T2修改状态变量,使其满足条件,并调用notify()方法;
7) T3获得锁,然后处理数据,并将状态变量又设置为不满足条件的状态;
8) T3处理完毕返回;
9) T1被唤醒,但实际上此时条件并不满足。
这个例子刚好印证了《Effective Java》中"Item 50: Never invoke wait outside a loop"和《Practical Java》中"实践54:针对wait()和notifyAll()使用旋锁(spin locks)"。即总是用下面这种方式来调用wait():
synchronized(obj) {
while(<condition does not hold>)
wait();
... // Perform action appropriate to condition
}
或者象《Practical Java》中一样:
synchronized(obj) {
while(<condition does not hold>) {
try {
wait();
} catch (InterruptedException e) {}
}
... // Perform action appropriate to condition
}
3. 调用wait()的线程T可能在以下几种情况被唤醒:
1) 其它线程调用了notify(),而刚好线程T得到了通知;
2) 其它线程调用了notifyAll();
3) 其它线程中断了线程T;
4) 由于JVM的原因,导致了spurious wakeup。
4.1.2 wait(), notify(), and notifyAll()1. 正像多个线程等待同一对象上的锁,当锁释放时,无法确定哪个线程会得到那个锁一样;当有多个线程在wait()时,当另外一个线程调用nofity()的时候,也不能确定哪个线程会被唤醒; 2. 因此在《Practical Java》的"实践53:优先使用notifyAll()而非notify()"建议的一样,结合实践54,可以比较好的解决线程唤醒的问题。
4.1.3 Wait-and-Notify Mechanism with Synchronized blocks再次强调必须在同一个对象的synchronized方法或块内调用该对象上的wait和notify方法。
4.2 Condition Variables
1. 就像上面反复强调的一样,wait-and-notify机制是与特定对象及其上的锁是绑定在一起的,锁和唤醒对象不能分开,这在某些情况下不是很方便;
2. JDK 1.5提供Condition接口来提供与其它系统几乎一致的condition variables机制;
3. Condition对象由Lock对象的newCondition()方法生成,从而允许一个锁产生多个条件变量,可以根据实际情况来等待不同条件;
4. 该书的例子没有什么特别的实际意义,但JDK 1.5文档中提供了一个例子,能充分说明使用Condition Variables使得程序更加清晰易读,也更有效率:
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();
}
}
}
具体的说明请参考JDK 1.5的文档。
5. 除了用lock和await-and-signal来代替synchronized和wait-and-notify外,其语义和机制基本一样。await()在进入前也会自动释放锁,然后再返回前重新获得锁;
6. 使用Condition Variables的原因:
1) 如果使用Lock对象,则必须使用condition variables;
2) 每个Lock对象可以创建多个condition variable.