原文中在前面还提到了死锁,饥饿进程和livelock的问题。这些都是在编写多线程程序时需要注意的问题。
参考原文:
http://download.oracle.com/javase/tutorial/essential/concurrency/liveness.html
但是今天来看一个更重要,实用的应用:在producer-consumer结构中使用的Guarded Blocks
链接是:http://download.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
producer-consumer
在进行多线程编程时使用的最多的结构是什么呢?那就是producer-consumer结构,即一个或多个线程produce一个资源,而一个或多个线程消费这个资源。
从后面的FTP传输文件的例子能够很好的理解这种结构,我们姑且继续往下讲。
Guarded Blocks
指当满足一定条件后才会执行的一段代码。
public void guardedJoy()
{ //简单的loop guard. 它会浪费处理器时间,所以不要这么做!
while(!joy) {}
System.out.println("Joy has been achieved!");
}
在上例中,就是当joy=false时,才会跳出wile循环,之后才会执行打印语句。while(!joy){}就是一个Guarded Blocks。
这种应用在程序中比比皆是,但是放在多线程中就引入了更复杂的问题:等待的对象是由其他进程提供的(即有多个线程消费这个资源)。
原文中提到了,while(!joy)会消耗cpu资源,不是一个好的结构。
更好的结构是:
A more efficient guard invokes Object.wait
to suspend the current thread (这样在等待时,就不会消耗任何资源。直到有其他线程将该线程唤醒).
The invocation of wait
does not return until another thread has issued a notification that some special event may have occurred — though not necessarily the event this thread is waiting for:
public synchronized guardedJoy()
{ //This guard only loops once for each special event, which may not be the event we're waiting for.
while(!joy) {
//wait会将当前线程挂起,所以性能更好。
try { wait();
}
catch (InterruptedException e) {
}
}
System.out.println("Joy and efficiency have been achieved!");
}
为什么这里使用了synchronized?
Supposed
is the object we're using to invoke wait
. When a thread invokesd.wait
, it must own the intrinsi clock ford
— otherwise an error is thrown. Invokingwait
inside a synchronized method is a simple way to acquire the intrinsic lock.
其他的thread在条件成熟(joy=true)之后,会用notify唤醒挂起的thread:
public synchronized notifyJoy()
{ joy = true;
notifyAll();
}
这段话太重要了:
Let's use guarded blocks to create a Producer-Consumerapplication. This kind of application shares data between two threads: theproducer, that creates the data, and the consumer, that does something with it. The two threads communicate using a shared object. Coordination is essential: the consumer thread must not attempt to retrieve the data before the producer thread has delivered it, and the producer thread must not attempt to deliver new data if the consumer hasn't retrieved the old data.
现在举一个简单的例子来帮助理解producer-consumer 应用。一个小程序一直扫描一个folder,当里面有文件时,就将文件传送到远程ftp服务器。
如果为了加快传输效率,使用多线程则面临一个问题:扫描folder很快,但传输文件需要一段时间。因此可以用一个线程扫描目录,用多线程传输文件。
在这个例子中,scanner就是producer,它扫描目录并将得到的file放入一个arraylist中,因此可以说,它是需要传输文件的“制造者”。
transfer就是consumer,它从arraylist中取出一个文件,上传到ftp后,同时将该file从arraylist中移除。因此可以说,它是传输文件的“消耗者”。
而这个arraylist就必须是scanner和transfer共享的资源。
这个小程序需要注意的问题,同时也是使用这种结构必须考虑的问题:
1, scanner将扫描到的file重命名后放入arraylist,比如.upload。这样是为了在文件尚未被upload而留在目录时,scanner不会将一个文件重复放入arraylist。重命名的工作必须由scanner完成,而不是transfer。因为如果文件较多,tranfer的多个线程将来不及rename所有的files,这时scanner就会重复读取一个文件。
2, transfer的多个线程从arraylist中取出需要upload的file时,要防止多个线程同时取出同一个file。这里就必须使用guide block以及同步方法。transfer线程上传文件后,不管成功与否,都必须将文件rename回initial name。成功的文件archive,不成功则会再次被scanner放入arraylist并试图再次上传。同时从arraylist取出一个要upload的文件时,将该文件从arraylist中移走。
3, scanner和transfer都应一直运行。除非main线程停止。
4, 用于tranfer线程的多少实现可配置。这里我们使用线程池,在下一节中介绍。
5,scanner和transfer都是运行时就根据结果做相应处理。即同一个线程负责上传文件,等待结果,进行下一步处理。因此scanner和transfer使用runnable即可。而不必使用callable或future。
关键的共享对象部分的代码是:
public class FileArray {
//Files sent from producer to consumer.
private ArrayList<File> FileArray = new ArrayList();
//True if consumer should wait for producer to send message, false
//if producer should wait for consumer to retrieve message.
private boolean empty = true;
public synchronized File take() {
//Wait until message is available.
while (empty) {
try {
wait();
} catch (InterruptedException e) {}
}
//Toggle status.
empty = true;
//Notify producer that status has changed.
notifyAll();
File file = FileArray.get(0);
FileArray.remove(file) ;
return file;
}
public synchronized void put(File file) {
//Wait until message has been retrieved.
while (!empty) {
try {
wait();
} catch (InterruptedException e) {}
}
//Toggle status.
empty = false;
//Store message.
this.FileArray.add(file) ;
//Notify consumer that status has changed.
notifyAll();
}
}
scanner的run方法:
public void run(){
while(true){
try {
scan();
//在方法里会调用FileArray.put(tempFile);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
tranfer的run方法:
public void run(){
while(true){
try {
transfer(fileArray.take(), retryTimes);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
同时主线程也使用sleep一直运行而不退出。
这个例子相当于我们自己写了一个FileArray类实现了多线程间的同步存取。其实完全可以使用blocking queue等线程安全对象。
简单又好用,以后我们再学习。
最后再说一点关于wait(),notify()和notifyAll(). 这两个都是Object对象上的final方法,很意外。因此可以直接使用:
wait();而不需要创建任何实例,也不是静态方法。
notify() : Wakes up a single thread that is waiting on this object's monitor.
具体请参考Object的API。