長期以來,多線程問題相當受到面試官的青睞。雖然我個人認為我們當中很少有人能真正獲得機會開發複雜的多線程應用(在過去的七年中,我得到了一個機會),但是理解多線程對增加你的信心很有用。之前,我討論了一個wait()和sleep()方法區別的問題,這次,我將會討論join()和yield()方法的區別。坦白的說,實際上我並沒有用過其中任何一個方法,所以,如果你感覺有不恰當的地方,請提出討論。
Java執行緒調度的一點背景
在各種各樣的執行緒中,Java虛擬機必須實作一個有優先權的、基於優先權的排程器。這意味著Java程式中的每一個執行緒被分配到一定的優先權,使用定義好的範圍內的一個正整數表示。優先順序可以被開發者改變。即使執行緒已經運行了一定時間,Java虛擬機也不會改變其優先權
優先權的值很重要,因為Java虛擬機和下層的作業系統之間的約定是作業系統必須選擇有最高優先權的Java執行緒運行。所以我們說Java實作了一個基於優先權的調度程式。這個調度程序使用一種有優先權的方式實現,這意味著當一個有更高優先權的線程到來時,無論低優先級的線程是否在運行,都會中斷(搶佔)它。這個約定對於作業系統來說並不總是這樣,這意味著作業系統有時可能會選擇運行一個更低優先權的執行緒。 (我憎恨多線程的這一點,因為這不能保證任何事情)
注意Java並不限定線程是以時間片運行,但是大多數操作系統卻有這樣的要求。在術語中經常引起混淆:搶佔經常與時間片混淆。事實上,搶佔意味著只有擁有高優先權的執行緒可以優先於低優先權的執行緒執行,但是當執行緒擁有相同優先權的時候,他們不能互相搶佔。它們通常受時間片管制,但這並不是Java的要求。
理解執行緒的優先權
接下來,理解執行緒優先權是多執行緒學習很重要的一步,尤其是了解yield()函數的工作過程。
記住當執行緒的優先權沒有指定時,所有執行緒都攜帶普通優先權。
優先權可以用從1到10的範圍來指定。 10表示最高優先級,1表示最低優先級,5是普通優先級。
記住優先順序最高的執行緒在執行時被給予優先。但是不能保證執行緒在啟動時就進入運行狀態。
與在執行緒池中等待運行機會的執行緒相比,目前正在運行的執行緒可能總是擁有更高的優先權。
由調度程式決定哪一個執行緒被執行。
t.setPriority()用來設定執行緒的優先權。
記住在執行緒開始方法被呼叫之前,執行緒的優先權應該被設定。
你可以使用常數,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY來設定優先權
現在,當我們對執行緒調度和執行緒優先權有一定理解後,讓我們進入主題。
yield()方法
理論上,yield意味著放手,放棄,投降。一個呼叫yield()方法的執行緒告訴虛擬機器它樂意讓其他執行緒佔用自己的位置。這表明該線程沒有在做一些緊急的事情。請注意,這僅是一個暗示,並不能保證不會產生任何影響。
在Thread.java中yield()定義如下:
/** * A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore * this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU. * Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect. */ public static native void yield();
讓我們列舉一下關於以上定義重要的幾點:
Yield是一個靜態的原生(native)方法
Yield把目前正在執行的執行緒告訴目前正在執行的執行緒。運行機會交給執行緒池中擁有相同優先權的執行緒。
Yield不能保證使得當前正在運行的線程迅速轉換到可運行的狀態
它只能使一個線程從運行狀態轉到可運行狀態,而不是等待或阻塞狀態
yield()方法使用示例
在下面的示例程序中,我随意的创建了名为生产者和消费者的两个线程。生产者设定为最小优先级,消费者设定为最高优先级。在Thread.yield()注释和非注释的情况下我将分别运行该程序。没有调用yield()方法时,虽然输出有时改变,但是通常消费者行先打印出来,然后事生产者。
调用yield()方法时,两个线程依次打印,然后将执行机会交给对方,一直这样进行下去。
package test.core.threads; public class YieldExample { public static void main(String[] args) { Thread producer = new Producer(); Thread consumer = new Consumer(); producer.setPriority(Thread.MIN_PRIORITY); //Min Priority consumer.setPriority(Thread.MAX_PRIORITY); //Max Priority producer.start(); consumer.start(); } } class Producer extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("I am Producer : Produced Item " + i); Thread.yield(); } } } class Consumer extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("I am Consumer : Consumed Item " + i); Thread.yield(); } } }
上述程序在没有调用yield()方法情况下的输出:
I am Consumer : Consumed Item 0 I am Consumer : Consumed Item 1 I am Consumer : Consumed Item 2 I am Consumer : Consumed Item 3 I am Consumer : Consumed Item 4 I am Producer : Produced Item 0 I am Producer : Produced Item 1 I am Producer : Produced Item 2 I am Producer : Produced Item 3 I am Producer : Produced Item 4
上述程序在调用yield()方法情况下的输出:
I am Producer : Produced Item 0 I am Consumer : Consumed Item 0 I am Producer : Produced Item 1 I am Consumer : Consumed Item 1 I am Producer : Produced Item 2 I am Consumer : Consumed Item 2 I am Producer : Produced Item 3 I am Consumer : Consumed Item 3 I am Producer : Produced Item 4 I am Consumer : Consumed Item 4
join()方法
线程实例的方法join()方法可以使得在另一个线程的执行结束后再开始执行这个线程。如果join()方法被在一个线程实例上调用,当前运行着的线程将阻塞直到线程实例完成了执行。
//Waits for this thread to die. public final void join() throws InterruptedException
在join()方法内设定超时,使得join()方法的影响在特定超时后无效。当超时时,主方法和任务线程申请运行的时候是平等的。然而,当涉及sleep时,join()方法依靠操作系统计时,所以你不应该假定join()方法将会等待你指定的时间。
像sleep,join通过抛出InterruptedException对中断做出回应。
join()方法使用示例
package test.core.threads; public class JoinExample { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { public void run() { System.out.println("First task started"); System.out.println("Sleeping for 2 seconds"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("First task completed"); } }); Thread t1 = new Thread(new Runnable() { public void run() { System.out.println("Second task completed"); } }); t.start(); // Line 15 t.join(); // Line 16 t1.start(); } } Output: First task started Sleeping for 2 seconds First task completed Second task completed