当有多个线程同时可以使用时,那就必须考虑到线程调度问题。需要保证每次线程至少有一点时间可以去run,重要的线程有更多的时间去run。
实际上,避免饿死要比避免错误同步或者死锁要容易得多。
Properties
并非每个创建的线程都是“同等”的,线程有属性,属性值从0-10,vm执行时,当有多个线程可执行时,vm会只执行属性值最高的。默认线程属性值是5,可设置。
一般会把1,5,10赋给下面3个变量
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
设置线程的properties
public final void setPriority(int newPriority)
如果newPriority大于最大的,或者负数,报异常IllegalArgumentException。
Preemption
每个vm都有线程调度器以在可以运行线程时决定运行哪个线程。
主要有2种调度器:抢占式(preemptive)和合作式(cooperative)。
抢占式:当需要时暂停正在执行的线程,切换到其他线程。
合作式:当正在执行的线程自己暂停时,才切换到其他线程。
运用合作式的方式更容易导致“饿死”。
所有的jvm都使用“抢占式”来调度,当一个低properties的线程正在运行时,一个高properties的线程ready to run,调度器会“很快”使低properties的线程暂停,而去使高properties的线程运行!
比较棘手的情况是当可以运行的线程的properties是一样的的时候,调度器该怎么办?抢占式的偶尔会让正在运行的线程暂停,让别的同properties的线程跑一会,而合作式的就不同了,它会一直等,直到正在运行的线程自己暂停或者到达某个停止点,如果这些情况没有出现,那别的线程只能都饿死了。所以让你所有的线程都周期性的暂停一下,以让其他线程有机会执行是很有必要的。
note:如果你基于一个使用抢占式调度算法的VM开发程序的话,很难会碰到饿死的现象。但在你的机器上不发生,不代表在客户上的机器上也不发生,比如,客户的VM使用的调度算法是合作式的呢?尽管现在大部分已使用抢占式,但老式的VM还是使用合作式,又或者有特殊目的的VM呢,比如嵌入式设备。
这里有10种方式表明一个可以暂停或者已准备好暂停。
<span style="font-size:18px;">• It can block on I/O.
• It can block on a synchronized object.
• It can yield.
• It can go to sleep.
• It can join another thread.
• It can wait on an object.
• It can finish.
• It can be preempted by a higher-priority thread.
• It can be suspended.
• It can stop.</span>
确保在你写的run()方法里上面的条件会以合理的频率出现。最后2个方法已弃用,因为会导致对象状态前后不一致的情况。
Blocking
blocking会发生在任何一个需要停下来等待它需要的资源的时候。网络程序最常见的被block是 I/O ,因为cpu的速度远比 network 和 disk的速度快。网络程序常在接受数据或发送数据时发送阻塞,即使这个时间只有多少毫秒,那也够其他线程做很显著的工作了。
线程还可能被synchronization代码块或者同步方法阻塞。如果没有拿到lock,那线程就会停止等待,直到拿到lock,永远拿不到就永远等待。
如果线程既没有被 I/O 阻塞,也没有被阻塞在lock上,那它会释放所有已经持有的lock。对 I/O block来说没什么大事,因为要么unblock,线程继续,要么抛出IOException,线程退出同步代码块或方法,释放所有locks。然而如果是阻塞在同步代码块或者方法上,谁也不放弃也有的lock,都在傻等对方有的lock,那结果就是死锁!
Yieling
使线程放弃cpu控制权的另一个方法是显式地调用yield,Thread.yield()!告诉jvm,如果现在有线程可以运行,那就去取运行它们吧,我暂时放弃运行的权利了!但在一些实时运行的jvm上,会忽略该方法。
在Yieling前,线程要确保它或与它相关的Runnable对象有前后一致的状态,因为该状态可能会被其他线程使用。Yielding不会使线程放弃已有的lock,所以,在yield前,要把持有的同步的lock,release掉。如果唯一等待运行的线程被block了,原因是它需要的lock被刚才yield的线程拿着呢,那这个线程就会block在那,而cpu执行权会又回到刚才yield的线程处,那这就违背了yield的目的了。
yield的方式很简单,比如在一个infinite的loop中,调用Thread.yield(),给其他同properties的线程运行的机会。
<span style="font-size:18px;">public void run() {
while (true) {
// Do the thread's work...
Thread.yield();
}
}</span>
Sleeping
yielding是表明一个线程愿意暂停,以给其他同properties的线程运行的机会。而sleeping则是一个将要暂停,不管有没有其他线程在等着执行。sleeping后,其他同properties或low properties的线程都有机会执行。线程sleep,会保持已经获得的lock,所以避免在同步代码块中sleep,因为其他线程拿到cpu控制权后发现需要的lock被sleep的线程拿着,那它会被block。
要使线程sleep的api是:
<span style="font-size:18px;">public static void sleep(long milliseconds) throws InterruptedException
public static void sleep(long milliseconds, int nanoseconds)
throws InterruptedException</span>
现代cpu的时钟接近毫秒的准确度,但纳秒的很少。并不能保证在任何vm上都能达到想要的sleep时间。如果本地硬件不支持需要的那个水平的时间准确度,那就用可计算的最接近的时间值。
可能线程在设定的时间没有wake up,只是因为VM在忙其他事,也可能,还没到设定的时间,就被其他线程wake up了。
<span style="font-size:18px;">public void interrupt()</span>
一个线程在sleep,并不表示其他work的线程就不能与其交互了,调用sleep线程的 interrupt()方法,sleep线程会抛出InterruptedException,然后就醒来,和正常线程没有区别了。在一个infinite loop thread 在sleep时,调用sleep线程的interrupt()方法,可以将其唤醒,infinite loop is broken,run method finished ,the thread dies.
如果一个线程在I/O操作上block了,interrupt()这个线程的效果与平台有关。通常情况下,是没有效果的,线程继续被blocked。如果你的程序需要可中断的I/O,那就要仔细考虑,使用非阻塞的I/O,而不是Streams。不像streams,buffere和channel明确被设计来支持中断的,当在read或write被block时。
Joining
一个线程经常需要其他线程的结果。比如,一个web浏览器在主线程加载html时,会spawn一个子线程去下载图片,如果图片的标签中没有heigh和width的话,那主线程需要等待所有的图片下载完成后才能去显示他们。
<span style="font-size:18px;">public final void join() throws InterruptedException
public final void join(long milliseconds) throws InterruptedException
public final void join(long milliseconds, int nanoseconds)
throws InterruptedException</span>
第一个方法会无限期的等待joined线程完成,其他方法等待指定的时间,时间到,即使joined线程未完成也会接着做自己的事。
the joinging thread(调用join方法的thread)会等待the joined thread(方法join被调用的thread)完成。
下面的例子中,求一个数组中的最大,最小,中间值。main thread是joining thread,SortThread是the joined thread。main thread 会等st执行完了,再执行,然后输出最大,最小,中间值。如果把注释的代码打开,那就像sleep的thread被interrupt一样,the joining thread也会被interrupt,抛出InterruptionException。
<span style="font-size:18px;"> public static void main(String[] args) {
// final Thread main = Thread.currentThread();
Double[] arrays = new Double[10000];
for(int i=0;i<arrays.length;i++){
arrays[i] = Math.random();
}
SortedThread st = new SortedThread(arrays);
st.start();
// Thread t = new Thread(new Runnable() {
//
// @Override
// public void run() {
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
//
// main.interrupt();
// }
// });
// t.start();
try {
st.join();
System.out.println("min:"+arrays[0]);
System.out.println("max:"+arrays[arrays.length-1]);
System.out.println("mid:"+arrays[arrays.length/2]);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}</span>
class SortedThread extends Thread{
private Double[] arr;
public SortedThread(Double[] arr){
this.arr = arr;
}
@Override
public void run() {
Arrays.sort(arr,new Comparator<Double>(){
@Override
public int compare(Double o1, Double o2) {
// TODO Auto-generated method stub
if(o1 > o2){
return 1;
}else if(o1 < o2){
return -1;
}else{
return 0;
}
}
});
}
}
note:jonin可能不如在jdk 5 之前那么重要了,因为可以用Executor和Future代替了,后者使用起来更方便。
Waiting on an Object
一个线程可以在一个它已经lock住的对象上等待。在等待时,线程会释放该对象的lock然后会一直pause,直到该对象被其他线程notified。其他线程可以改变object,然后notify在该object上等待的线程,notify后自己继续执行。wait不同于join,在等待线程和唤醒线程间没有谁必须等对象结束了才能继续执行,waiting暂停一个线程的执行直到某个对象或资源达到某个状态,joining暂停一个线程的执行直到另一个线程执行完!
在一个对象上Waiting可使一个线程pause,知道这个的人并不多,因为没有调用任何Thread的方法。一个想要通过waiting object pause的线程,先要通过synchronized获得该object的lock,然后调用object的以下方法:
public final void wait() throws InterruptedException
public final void wait(long milliseconds) throws InterruptedException
public final void wait(long milliseconds, int nanoseconds)
throws InterruptedException
上述方法一被调用,在其上waiting的线程即释放掉该object的lock,进入sleep状态,直到有以下条件之一发生:
• The timeout expires.
• The thread is interrupted.
• The object is notified.
timeout就像sleep,join一样,时间到后,线程恢复执行, 从wait()后开始。但是,如果不能立马获得object的lock的话,会被block,等待获得lock。
interrupt也像sleep、join一样,其他线程调用正pause线程的interrupt()方法,抛出InterruptionException,从Catch处接着执行,但仍需要先获得object的lock,在获得lock前不会抛出异常,所以,可能其他线程调用interrupt()后,waiting线程会被blocked一段时间。
第三种方式,notify,要调用waiting线程所wait的对象的notify,或notifyAll方法,其他线程在调用这2个方法前,要先获得object的lock,并要synchronized,notify会在等待线程中随机选一个,notifyAll会使所有在该对象上等待的线程都恢复工作,waiting线程所等待的对象呗notify后,waiting线程会从wait()后开始执行。
现在有一个这样的例子,一个线程从网络上读取一个文件,这个文件先读到的部分是Manifest文件,而另一个线程对Menifest很感兴趣,它希望先拿到Manifest的数据,等整个文件都接收完成了再读其他数据。
ManifestFile m = new ManifestFile();
JarThread t = new JarThread(m, in);
synchronized (m) {
t.start();
try {
m.wait();
// work with the manifest file...
} catch (InterruptedException ex) {
// handle exception...
}
}
JarThread
ManifestFile theManifest;
InputStream in;
public JarThread(Manifest m, InputStream in) {
theManifest = m;
this.in= in;
}
@Override
public void run() {
synchronized (theManifest) {
// read the manifest from the stream in...
theManifest.notify();
}
// read the rest of the stream...
}
在相同对象上的waiting和notification在多线程编程中是很常见的。
现在有有一个这样的例子:多线程处理log,用另一个专门的线程读取log,并放在List中,处理log的线程从List中获取log。
public static void main(String[] args) {
List<Log> logList = new LinkedList<Log>();
Thread[] proArr = new Thread[3];
for(int i=0;i<proArr.length;i++){
proArr[i] = new ProcessLogThread(logList);
proArr[i].start();
}
ReadLogThread rt = new ReadLogThread(logList, proArr);
rt.start();
}
class ReadLogThread extends Thread{
private List<Log> logList;
private Thread[] processArr;
private int logPoint;
public ReadLogThread(List<Log> logList,Thread[] processArr){
this.logList = logList;
this.processArr = processArr;
}
@Override
public void run() {
while(true){
Log log = getNextLog();
if(null == log){
while(logList.size() > 0){} //如果此时还有log没有被处理,那就等着处理,尽管这种情况不大可能发生
for(Thread p : processArr) p.interrupt(); //打断所有pause的线程
break;
}
synchronized (logList) {
logList.add(log);
logList.notifyAll();
}
}
}
private Log getNextLog() {
if(logPoint >= 1000) return null;
return new Log("log"+logPoint++);
}
}
class ProcessLogThread extends Thread{
private List<Log> logList;
public ProcessLogThread(List<Log> logList){
this.logList = logList;
}
@Override
public void run() {
while(true){
synchronized (logList) {
while(logList.isEmpty()){ //这里使用while的原因是当线程被notify后,获得logList的lock时可能logList
//中已经没有log了,被其他线程处理完了,所以判断如果没有了,那就继续wait
try {
logList.wait();
} catch (InterruptedException e) {
return;
}
}
Log log = logList.remove(logList.size()-1);
System.out.println(Thread.currentThread().getName()+" process log:"+log.getLog());
}
}
}
}
class Log {
private String log;
public Log(String log){
this.log = log;
}
public String getLog(){
return log;
}
}
这里我曾考虑用“观察者模式”实现当List中添加数据了,就通知处理线程去工作,但观察者的本质是“回调”,实际执行处理的还是通知的线程,所以观察者在这里不合适。
Finish
最后的一个线程放弃cpu执行权的是finishing !当run()方法结束,线程dies,其他线程接管。另外,如果你的线程每次都非常快的都结束了,以致都没有blocking,那有必要spawn一个线程吗,为啥不用个方法呢?vm创建销毁一个线程开销也是很大的!
++++++++++++++++++++++++++++++++++++++++++++++++
现在有一个任务,要求线程B在执行时需要线程A产生的某个结果,线程B只要线程A中间的某个结果,并不需要线程A执行完毕,因此没必要用join(因为the joinging thread(调用join方法的thread)会等待the joined thread(方法join被调用的thread)完成),这里可以在线程B需要资源时调用某个唯一对象的wait,然后等待线程A产生资源后,再调用同一对象的notify方法。
class TaskA implements Runnable{
private MyData myData;
public TaskA(MyData myData){
this.myData = myData;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (myData) {
try {
System.out.println("TaskA will wait...");
myData.wait();
System.out.println("TaskA Inside1 "+myData.data); //当调用myData.notify()后,执行myData.wait()后面的语句
} catch (InterruptedException e) {
//如果执行该Task的线程被interrupt,则执行catch中的内容
e.printStackTrace();
System.out.println("TaskA Inside2 "+myData.data);
}
}
}
}
class MyData{
public int data = 0;
}
public static void main(String[] args) {
MyData myData = new MyData();
TaskA ta = new TaskA(myData);
new Thread(ta).start();
System.out.println("main thread is sleeping...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.data = 3;
synchronized (myData) {
myData.notify();
}
}
Sleep和Yield比较
同:都是Thread的静态方法,都可以让渡cpu执行权
异:在让渡cpu执行权方面,可以指定一定的时间,sleep更好用,如果调用yield的目的是让渡cpu,完全可以sleep(1)来代替。 sleep不会让当前执行线程在sleeping时放弃很多monitor。 当sleep的线程被interrupt时抛出InterruptedExcepiton ,而yield不会抛任何异常。
所以,可以忘记yield,使用sleep吧。