我們知道多線程是Java編程中重要的一塊內容,也是面試重點覆蓋區域,所以學好多線程對我們來說極其重要,下面跟我一起開啟本次的學習之旅吧。
一、線程基本概念
1 線程:進程中負責程序執行的執行單元(執行路徑)
線程本身依靠程序進行運行
線程是程序中的順序控制流,只能使用分配給程序的資源和環境
2 進程:執行中的程序
一個進程至少包含一個線程
3 單線程:程序中只存在一個線程,實際上主方法就是一個主線程
4 多線程:在一個程序中運行多個任務
目的是更好地使用CPU資源
用多線程只有一個目的,那就是更好的利用cpu的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程序代碼,最起碼每個角色要給他一個線程吧,否則連實際場景都無法模擬,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。
很多人都對其中的一些概念不夠明確,如同步、並發等等,讓我們先建立一個數據字典,以免產生誤會。
多線程:指的是這個程序(一個進程)運行時產生了不止一個線程。
並行與並發:
並行:多個cpu實例或者多台機器同時執行一段處理邏輯,是真正的同時。
並發:通過cpu調度算法,讓用戶看上去同時執行,實際上從cpu操作層面不是真正的同時。並發往往在場景中有公用的資源,那么針對這個公用的資源往往產生瓶頸,我們會用TPS或者QPS來反應這個系統的處理能力。
二、線程的狀態
線程狀態:
說明:
線程共包括以下5種狀態。
1. 新建狀態(New) : 線程對象被創建后,就進入了新建狀態。例如,Thread thread = new Thread()。
2. 就緒狀態(Runnable): 也被稱為“可執行狀態”。線程對象被創建后,其它線程調用了該對象的start()方法,從而來啟動該線程。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
3. 運行狀態(Running) : 線程獲取CPU權限進行執行。需要注意的是,線程只能從就緒狀態進入到運行狀態。
4. 阻塞狀態(Blocked) : 阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
(01) 等待阻塞 -- 通過調用線程的wait()方法,讓線程等待某工作的完成。
(02) 同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。
(03) 其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5. 死亡狀態(Dead) : 線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
這5種狀態涉及到的內容包括Object類, Thread和synchronized關鍵字。這些內容我們會在后面的章節中逐個進行學習。
Object類,定義了wait(), notify(), notifyAll()等休眠/喚醒函數。
Thread類,定義了一些列的線程操作函數。例如,sleep()休眠函數, interrupt()中斷函數, getName()獲取線程名稱等。
synchronized,是關鍵字;它區分為synchronized代碼塊和synchronized方法。synchronized的作用是讓線程獲取對象的同步鎖。
在后面詳細介紹wait(),notify()等方法時,我們會分析為什么“wait(), notify()等方法要定義在Object類,而不是Thread類中”。
注:sleep和wait的區別:【考點】
sleep
是Thread
類的方法,wait
是Object
類中定義的方法.Thread.sleep
不會導致鎖行為的改變, 如果當前線程是擁有鎖的, 那么Thread.sleep
不會讓線程釋放鎖.Thread.sleep
和Object.wait
都會暫停當前的線程. OS會將執行時間分配給其它線程. 區別是, 調用wait
后, 需要別的線程執行notify/notifyAll
才能夠重新獲得CPU執行時間.
三、線程的創建
線程的創建方式為:
1.繼承Thread
類
在java.lang
包中定義, 繼承Thread類必須重寫run()
方法
class MyThread extends Thread{ private static int num = 0; public MyThread(){ num++; } @Override public void run() { System.out.println("主動創建的第"+num+"個線程"); } }
創建好了自己的線程類之后,就可以創建線程對象了,然后通過start()方法去啟動線程。注意,不是調用run()方法啟動線程,run方法中只是定義需要執行的任務,如果調用run方法,即相當於在主線程中執行run方法,跟普通的方法調用沒有任何區別,此時並不會創建一個新的線程來執行定義的任務。
public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } class MyThread extends Thread{ private static int num = 0; public MyThread(){ num++; } @Override public void run() { System.out.println("主動創建的第"+num+"個線程"); } }
在上面代碼中,通過調用start()方法,就會創建一個新的線程了。為了分清start()方法調用和run()方法調用的區別,請看下面一個例子:
public class Test { public static void main(String[] args) { System.out.println("主線程ID:"+Thread.currentThread().getId()); MyThread thread1 = new MyThread("thread1"); thread1.start(); MyThread thread2 = new MyThread("thread2"); thread2.run(); } } class MyThread extends Thread{ private String name; public MyThread(String name){ this.name = name; } @Override public void run() { System.out.println("name:"+name+" 子線程ID:"+Thread.currentThread().getId()); } }
運行結果:
從輸出結果可以得出以下結論:
1)thread1和thread2的線程ID不同,thread2和主線程ID相同,說明通過run方法調用並不會創建新的線程,而是在主線程中直接運行run方法,跟普通的方法調用沒有任何區別;
2)雖然thread1的start方法調用在thread2的run方法前面調用,但是先輸出的是thread2的run方法調用的相關信息,說明新線程創建的過程不會阻塞主線程的后續執行。
2.實現Runnable
接口
在Java中創建線程除了繼承Thread類之外,還可以通過實現Runnable接口來實現類似的功能。實現Runnable接口必須重寫其run方法。
public class Test { public static void main(String[] args) { System.out.println("主線程ID:"+Thread.currentThread().getId()); MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } } class MyRunnable implements Runnable{ public MyRunnable() { } @Override public void run() { System.out.println("子線程ID:"+Thread.currentThread().getId()); } }
Runnable的中文意思是“任務”,顧名思義,通過實現Runnable接口,我們定義了一個子任務,然后將子任務交由Thread去執行。注意,這種方式必須將Runnable作為Thread類的參數,然后通過Thread的start方法來創建一個新線程來執行該子任務。如果調用Runnable的run方法的話,是不會創建新線程的,這根普通的方法調用沒有任何區別。
事實上,查看Thread類的實現源代碼會發現Thread類是實現了Runnable接口的。引出靜態代理模式:
線程體(也就是我們要執行的具體任務)實現了Runnable接口和run方法。同時Thread類也實現了Runnable接口。此時,線程體就相當於目標角色,Thread就相當於代理角色。當程序調用了Thread的start()方法后,Thread的run()方法會在某個特定的時候被調用。thread.run()方法:
public void run() { if (target != null) { target.run(); } }
說明:target是一個Runnable對象。run()就是直接調用Thread線程的Runnable成員的run()方法,並不會新建一個線程。
在Java中,這2種方式都可以用來創建線程去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現Runnable接口看起來更加簡潔,但是由於Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現Runnable接口。
3.使用ExecutorService、Callable、Future實現有返回結果的多線程
上面我們發現都是沒有辦法獲取到線程執行的返回結果的,為了解決這個問題,於是出現了Callable;並發里面會繼續學到,這里暫時先知道一下有這種方法即可。
ExecutorService、Callable、Future這個對象實際上都是屬於Executor框架中的功能類。想要詳細了解Executor框架的可以訪問http://www.javaeye.com/topic/366591,這里面對該框架做了很詳細的解釋。返回結果的線程是在JDK1.5中引入的新特征,確實很實用,有了這種特征我就不需要再為了得到返回值而大費周折了,而且即便實現了也可能漏洞百出。
可返回值的任務必須實現Callable接口,類似的,無返回值的任務必須Runnable接口。執行Callable任務后,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務返回的Object了,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。下面提供了一個完整的有返回結果的多線程測試例子。代碼如下:
/** * 有返回值的線程 */ @SuppressWarnings("unchecked") public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println("----程序開始運行----"); Date date1 = new Date(); int taskSize = 5; // 創建一個線程池 ExecutorService pool = Executors.newFixedThreadPool(taskSize); // 創建多個有返回值的任務 List<Future> list = new ArrayList<Future>(); for (int i = 0; i < taskSize; i++) { Callable c = new MyCallable(i + " "); // 執行任務並獲取Future對象 Future f = pool.submit(c); // System.out.println(">>>" + f.get().toString()); list.add(f); } // 關閉線程池 pool.shutdown(); // 獲取所有並發任務的運行結果 for (Future f : list) { // 從Future對象上獲取任務的返回值,並輸出到控制台 System.out.println(">>>" + f.get().toString()); } Date date2 = new Date(); System.out.println("----程序結束運行----,程序運行時間【" + (date2.getTime() - date1.getTime()) + "毫秒】"); } } class MyCallable implements Callable<Object> { private String taskNum; MyCallable(String taskNum) { this.taskNum = taskNum; } public Object call() throws Exception { System.out.println(">>>" + taskNum + "任務啟動"); Date dateTmp1 = new Date(); Thread.sleep(1000); Date dateTmp2 = new Date(); long time = dateTmp2.getTime() - dateTmp1.getTime(); System.out.println(">>>" + taskNum + "任務終止"); return taskNum + "任務返回運行結果,當前任務時間【" + time + "毫秒】"; } }
代碼說明:
上述代碼中Executors類,提供了一系列工廠方法用於創先線程池,返回的線程池都實現了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
創建固定數目線程的線程池。
public static ExecutorService newCachedThreadPool()
創建一個可緩存的線程池,調用execute 將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鍾未被使用的線程。
public static ExecutorService newSingleThreadExecutor()
創建一個單線程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
創建一個支持定時及周期性的任務執行的線程池,多數情況下可用來替代Timer類。
ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor后台線程池還沒有完成Callable的計算,這調用返回Future對象的get()方法,會阻塞直到計算完成。
四、線程的信息
對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能運行一個線程,當在運行一個線程的過程中轉去運行另外一個線程,這個叫做線程上下文切換(對於進程也是類似)。
由於可能當前線程的任務並沒有執行完畢,所以在切換時需要保存線程的運行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態運行。舉個簡單的例子:比如一個線程A正在讀取一個文件的內容,正讀到文件的一半,此時需要暫停線程A,轉去執行線程B,當再次切換回來執行線程A的時候,我們不希望線程A又從文件的開頭來讀取。
因此需要記錄線程A的運行狀態,那么會記錄哪些數據呢?因為下次恢復時需要知道在這之前當前線程已經執行到哪條指令了,所以需要記錄程序計數器的值,另外比如說線程正在進行某個計算的時候被掛起了,那么下次繼續執行的時候需要知道之前掛起時變量的值時多少,因此需要記錄CPU寄存器的狀態。所以一般來說,線程上下文切換過程中會記錄程序計數器、CPU寄存器狀態等數據。
說簡單點的:對於線程的上下文切換實際上就是 存儲和恢復CPU狀態的過程,它使得線程執行能夠從中斷點恢復執行。
雖然多線程可以使得任務執行的效率得到提升,但是由於在線程切換時同樣會帶來一定的開銷代價,並且多個線程會導致系統資源占用的增加,所以在進行多線程編程時要注意這些因素。
1.線程的常用方法
編號 | 方法 | 說明 |
---|---|---|
1 | public void start() |
使該線程開始執行;Java 虛擬機調用該線程的 run 方法。 |
2 | public void run() |
如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。 |
3 | public final void setName(String name) |
改變線程名稱,使之與參數 name 相同。 |
4 | public final void setPriority(int priority) |
更改線程的優先級。 |
5 | public final void setDaemon(boolean on) |
將該線程標記為守護線程或用戶線程。 |
6 | public final void join(long millisec) |
等待該線程終止的時間最長為 millis 毫秒。 |
7 | public void interrupt() |
中斷線程。 |
8 | public final boolean isAlive() |
測試線程是否處於活動狀態。 |
9 | public static void yield() |
暫停當前正在執行的線程對象,並執行其他線程。 |
10 | public static void sleep(long millisec) |
在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和准確性的影響。 |
11 | public static Thread currentThread() |
返回對當前正在執行的線程對象的引用。 |
1)currentThread()方法
currentThread()方法可以返回代碼段正在被哪個線程調用的信息
public class Run1{ public static void main(String[] args){ System.out.println(Thread.currentThread().getName()); } } ``` ### sleep()方法 方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的線程”休眠(暫停執行)。這個“正在執行的線程”是指this.currentThread()返回的線程。 sleep方法有兩個重載版本: ```java sleep(long millis) //參數為毫秒 sleep(long millis,int nanoseconds) //第一參數為毫秒,第二個參數為納秒
sleep相當於讓線程睡眠,交出CPU,讓CPU去執行其他的任務。
但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當前線程持有對某個對象的鎖,則即使調用sleep方法,其他線程也無法訪問這個對象。看下面這個例子就清楚了:
public class Test { private int i = 10; private Object object = new Object(); public static void main(String[] args) throws IOException { Test test = new Test(); MyThread thread1 = test.new MyThread(); MyThread thread2 = test.new MyThread(); thread1.start(); thread2.start(); } class MyThread extends Thread{ @Override public void run() { synchronized (object) { i++; System.out.println("i:"+i); try { System.out.println("線程"+Thread.currentThread().getName()+"進入睡眠狀態"); Thread.currentThread().sleep(10000); } catch (InterruptedException e) { // TODO: handle exception } System.out.println("線程"+Thread.currentThread().getName()+"睡眠結束"); i++; System.out.println("i:"+i); } } } }
上面輸出結果可以看出,當Thread-0進入睡眠狀態之后,Thread-1並沒有去執行具體的任務。只有當Thread-0執行完之后,此時Thread-0釋放了對象鎖,Thread-1才開始執行。
注意,如果調用了sleep方法,必須捕獲InterruptedException異常或者將該異常向上層拋出。當線程睡眠時間滿后,不一定會立即得到執行,因為此時可能CPU正在執行其他的任務。所以說調用sleep方法相當於讓線程進入阻塞狀態。
2.yield()方法
yield()的作用是讓步。它能讓當前線程由“運行狀態”進入到“就緒狀態”,從而讓其它具有相同優先級的等待線程獲取執行權;但是,並不能保證在當前線程調用yield()之后,其它具有相同優先級的線程就一定能獲得執行權;也有可能是當前線程又進入到“運行狀態”繼續運行!
// YieldTest.java的源碼 class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public synchronized void run(){ for(int i=0; i <10; i++){ System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i); // i整除4時,調用yield if (i%4 == 0) Thread.yield(); } } } public class YieldTest{ public static void main(String[] args){ ThreadA t1 = new ThreadA("t1"); ThreadA t2 = new ThreadA("t2"); t1.start(); t2.start(); } }
結果:
t1 [5]:0 t2 [5]:0 t1 [5]:1 t1 [5]:2 t1 [5]:3 t1 [5]:4 t1 [5]:5 t1 [5]:6 t1 [5]:7 t1 [5]:8 t1 [5]:9 t2 [5]:1 t2 [5]:2 t2 [5]:3 t2 [5]:4 t2 [5]:5 t2 [5]:6 t2 [5]:7 t2 [5]:8 t2 [5]:9
“線程t1”在能被4整數的時候,並沒有切換到“線程t2”。這表明,yield()雖然可以讓線程由“運行狀態”進入到“就緒狀態”;但是,它不一定會讓其它線程獲取CPU執行權(即,其它線程進入到“運行狀態”),即使這個“其它線程”與當前調用yield()的線程具有相同的優先級。
注意:
我們知道,wait()的作用是讓當前線程由“運行狀態”進入“等待(阻塞)狀態”的同時,也會釋放同步鎖。而yield()的作用是讓步,它也會讓當前線程離開“運行狀態”。它們的區別是:
(01) wait()是讓線程由“運行狀態”進入到“等待(阻塞)狀態”,而不yield()是讓線程由“運行狀態”進入到“就緒狀態”。
(02) wait()是會線程釋放它所持有對象的同步鎖,而yield()方法不會釋放鎖。
// YieldLockTest.java 的源碼 public class YieldLockTest{ private static Object obj = new Object(); public static void main(String[] args){ ThreadA t1 = new ThreadA("t1"); ThreadA t2 = new ThreadA("t2"); t1.start(); t2.start(); } static class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public void run(){ // 獲取obj對象的同步鎖 synchronized (obj) { for(int i=0; i <10; i++){ System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i); // i整除4時,調用yield if (i%4 == 0) Thread.yield(); } } } } }
結果:
t1 [5]:0 t1 [5]:1 t1 [5]:2 t1 [5]:3 t1 [5]:4 t1 [5]:5 t1 [5]:6 t1 [5]:7 t1 [5]:8 t1 [5]:9 t2 [5]:0 t2 [5]:1 t2 [5]:2 t2 [5]:3 t2 [5]:4 t2 [5]:5 t2 [5]:6 t2 [5]:7 t2 [5]:8 t2 [5]:9
結果說明:
主線程main中啟動了兩個線程t1和t2。t1和t2在run()會引用同一個對象的同步鎖,即synchronized(obj)。在t1運行過程中,雖然它會調用Thread.yield();但是,t2是不會獲取cpu執行權的。因為,t1並沒有釋放“obj所持有的同步鎖”!
3.start()方法
start()用來啟動一個線程,當調用start方法后,系統才會開啟一個新的線程來執行用戶定義的子任務,在這個過程中,會為相應的線程分配需要的資源。
4.run()方法
run()方法是不需要用戶來調用的,當通過start方法啟動一個線程之后,當線程獲得了CPU執行時間,便進入run方法體去執行具體的任務。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執行的任務。
5.getId()
getId()的作用是取得線程的唯一標識
public class Test { public static void main(String[] args) { Thread t= Thread.currentThread(); System.out.println(t.getName()+" "+t.getId()); } }
#### isAlive()方法 方法isAlive()的功能是判斷當前線程是否處於活動狀態 代碼: ```java public class MyThread extends Thread{ @Override public void run() { System.out.println("run="+this.isAlive()); } } public class RunTest { public static void main(String[] args) throws InterruptedException { MyThread myThread=new MyThread(); System.out.println("begin =="+myThread.isAlive()); myThread.start(); System.out.println("end =="+myThread.isAlive()); } }
程序運行結果:
begin ==false run=true end ==false
方法isAlive()的作用是測試線程是否偶處於活動狀態。什么是活動狀態呢?活動狀態就是線程已經啟動且尚未終止。線程處於正在運行或准備開始運行的狀態,就認為線程是“存活”的。
有個需要注意的地方
System.out.println("end =="+myThread.isAlive());
雖然上面的實例中打印的值是true,但此值是不確定的。打印true值是因為myThread線程還未執行完畢,所以輸出true。如果代碼改成下面這樣,加了個sleep休眠:
public static void main(String[] args) throws InterruptedException { MyThread myThread=new MyThread(); System.out.println("begin =="+myThread.isAlive()); myThread.start(); Thread.sleep(1000); System.out.println("end =="+myThread.isAlive()); }
則上述代碼運行的結果輸出為false,因為mythread對象已經在1秒之內執行完畢。
6.join()方法
join() 的作用:讓“主線程”等待“子線程”結束之后才能繼續運行。這句話可能有點晦澀,我們還是通過例子去理解:
// 主線程 public class Father extends Thread { public void run() { Son s = new Son(); s.start(); s.join(); ... } } // 子線程 public class Son extends Thread { public void run() { ... } }
說明:
上面的有兩個類Father(主線程類)和Son(子線程類)。因為Son是在Father中創建並啟動的,所以,Father是主線程類,Son是子線程類。
在Father主線程中,通過new Son()新建“子線程s”。接着通過s.start()啟動“子線程s”,並且調用s.join()。在調用s.join()之后,Father主線程會一直等待,直到“子線程s”運行完畢;在“子線程s”運行完畢之后,Father主線程才能接着運行。 這也就是我們所說的“join()的作用,是讓主線程會等待子線程結束之后才能繼續運行”!
實例:
// JoinTest.java的源碼 public class JoinTest{ public static void main(String[] args){ try { ThreadA t1 = new ThreadA("t1"); // 新建“線程t1” t1.start(); // 啟動“線程t1” t1.join(); // 將“線程t1”加入到“主線程main”中,並且“主線程main()會等待它的完成” System.out.printf("%s finish\n", Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } static class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public void run(){ System.out.printf("%s start\n", this.getName()); // 延時操作 for(int i=0; i <1000000; i++) ; System.out.printf("%s finish\n", this.getName()); } } }
運行結果:
t1 start
t1 finish
main finish
結果說明:
運行流程如圖
(01) 在“主線程main”中通過 new ThreadA("t1") 新建“線程t1”。 接着,通過 t1.start() 啟動“線程t1”,並執行t1.join()。
(02) 執行t1.join()之后,“主線程main”會進入“阻塞狀態”等待t1運行結束。“子線程t1”結束之后,會喚醒“主線程main”,“主線程”重新獲取cpu執行權,繼續運行。
7.getName和setName
用來得到或者設置線程名稱。
8.getPriority和setPriority
用來獲取和設置線程優先級
9.setDaemon和isDaemon
用來設置線程是否成為守護線程和判斷線程是否是守護線程。
守護線程和用戶線程的區別在於:守護線程依賴於創建它的線程,而用戶線程則不依賴。舉個簡單的例子:如果在main線程中創建了一個守護線程,當main方法運行完畢之后,守護線程也會隨着消亡。而用戶線程則不會,用戶線程會一直運行直到其運行完畢。在JVM中,像垃圾收集器線程就是守護線程。
在上面已經說到了Thread類中的大部分方法,那么Thread類中的方法調用到底會引起線程狀態發生怎樣的變化呢?下面一幅圖就是在上面的圖上進行改進而來的:
2.停止線程
停止線程是在多線程開發時很重要的技術點,掌握此技術可以對線程的停止進行有效的處理。
停止一個線程可以使用Thread.stop()方法,但最好不用它。該方法是不安全的,已被棄用。
在Java中有以下3種方法可以終止正在運行的線程:
- 使用退出標志,使線程正常退出,也就是當run方法完成后線程終止
- 使用stop方法強行終止線程,但是不推薦使用這個方法,因為stop和suspend及resume一樣,都是作廢過期的方法,使用他們可能產生不可預料的結果。
- 使用interrupt方法中斷線程,但這個不會終止一個正在運行的線程,還需要加入一個判斷才可以完成線程的停止。(可參考:http://www.cnblogs.com/skywang12345/p/3479949.html)
3.線程的優先級
在操作系統中,線程可以划分優先級,優先級較高的線程得到的CPU資源較多,也就是CPU優先執行優先級較高的線程對象中的任務。
設置線程優先級有助於幫“線程規划器”確定在下一次選擇哪一個線程來優先執行。
設置線程的優先級使用setPriority()方法,此方法在JDK的源碼如下:
public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } }
在Java中,線程的優先級分為1~10這10個等級,如果小於1或大於10,則JDK拋出異常throw new IllegalArgumentException()。
JDK中使用3個常量來預置定義優先級的值,代碼如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
線程優先級特性:
- 繼承性
比如A線程啟動B線程,則B線程的優先級與A是一樣的。 - 規則性
高優先級的線程總是大部分先執行完,但不代表高優先級線程全部先執行完。 - 隨機性
優先級較高的線程不一定每一次都先執行完。
4.守護線程
在Java線程中有兩種線程,一種是User Thread(用戶線程),另一種是Daemon Thread(守護線程)。
Daemon的作用是為其他線程的運行提供服務,比如說GC線程。其實User Thread線程和Daemon Thread守護線程本質上來說去沒啥區別的,唯一的區別之處就在虛擬機的離開:如果User Thread全部撤離,那么Daemon Thread也就沒啥線程好服務的了,所以虛擬機也就退出了。
守護線程並非虛擬機內部可以提供,用戶也可以自行的設定守護線程,方法:public final void setDaemon(boolean on) ;但是有幾點需要注意:
-
thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。 (備注:這點與守護進程有着明顯的區別,守護進程是創建后,讓進程擺脫原會話的控制+讓進程擺脫原進程組的控制+讓進程擺脫原控制終端的控制;所以說寄托於虛擬機的語言機制跟系統級語言有着本質上面的區別)
-
在Daemon線程中產生的新線程也是Daemon的。 (這一點又是有着本質的區別了:守護進程fork()出來的子進程不再是守護進程,盡管它把父進程的進程相關信息復制過去了,但是子進程的進程的父進程不是init進程,所謂的守護進程本質上說就是“父進程掛掉,init收養,然后文件0,1,2都是/dev/null,當前目錄到/”)
-
不是所有的應用都可以分配給Daemon線程來進行服務,比如讀寫操作或者計算邏輯。因為在Daemon Thread還沒來的及進行操作時,虛擬機可能已經退出了。
5.sleep
sleep() 的作用是讓當前線程休眠,即當前線程會從“運行狀態”進入到“休眠(阻塞)狀態”。sleep()會指定休眠時間,線程休眠的時間會大於/等於該休眠時間;在線程重新被喚醒時,它會由“阻塞狀態”變成“就緒狀態”,從而等待cpu的調度執行。
// SleepTest.java的源碼 class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public synchronized void run() { try { for(int i=0; i <10; i++){ System.out.printf("%s: %d\n", this.getName(), i); // i能被4整除時,休眠100毫秒 if (i%4 == 0) Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class SleepTest{ public static void main(String[] args){ ThreadA t1 = new ThreadA("t1"); t1.start(); } }
運行結果:
t1: 0 t1: 1 t1: 2 t1: 3 t1: 4 t1: 5 t1: 6 t1: 7 t1: 8 t1: 9
結果說明:
程序比較簡單,在主線程main中啟動線程t1。t1啟動之后,當t1中的計算i能被4整除時,t1會通過Thread.sleep(100)休眠100毫秒。
我們知道,wait()的作用是讓當前線程由“運行狀態”進入“等待(阻塞)狀態”的同時,也會釋放同步鎖。而sleep()的作用是也是讓當前線程由“運行狀態”進入到“休眠(阻塞)狀態”。
但是,wait()會釋放對象的同步鎖,而sleep()則不會釋放鎖。
6.interrupt
interrupt()的作用是中斷本線程。
本線程中斷自己是被允許的;其它線程調用本線程的interrupt()方法時,會通過checkAccess()檢查權限。這有可能拋出SecurityException異常。
如果本線程是處於阻塞狀態:調用線程的wait(), wait(long)或wait(long, int)會讓它進入等待(阻塞)狀態,或者調用線程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也會讓它進入阻塞狀態。若線程在阻塞狀態時,調用了它的interrupt()方法,那么它的“中斷狀態”會被清除並且會收到一個InterruptedException異常。例如,線程通過wait()進入阻塞狀態,此時通過interrupt()中斷該線程;調用interrupt()會立即將線程的中斷標記設為“true”,但是由於線程處於阻塞狀態,所以該“中斷標記”會立即被清除為“false”,同時,會產生一個InterruptedException的異常。
如果線程被阻塞在一個Selector選擇器中,那么通過interrupt()中斷它時;線程的中斷標記會被設置為true,並且它會立即從選擇操作中返回。
如果不屬於前面所說的情況,那么通過interrupt()中斷線程時,它的中斷標記會被設置為“true”。
中斷一個“已終止的線程”不會產生任何操作。
7.wait(), notify(), notifyAll()
在Object.java中,定義了wait(), notify()和notifyAll()等接口。wait()的作用是讓當前線程進入等待狀態,同時,wait()也會讓當前線程釋放它所持有的鎖。而notify()和notifyAll()的作用,則是喚醒當前對象上的等待線程;notify()是喚醒單個線程,而notifyAll()是喚醒所有的線程。
Object類中關於等待/喚醒的API詳細信息如下:
notify() -- 喚醒在此對象監視器上等待的單個線程。
notifyAll() -- 喚醒在此對象監視器上等待的所有線程。
wait() -- 讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法”,當前線程被喚醒(進入“就緒狀態”)。
wait(long timeout) -- 讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量”,當前線程被喚醒(進入“就緒狀態”)。
wait(long timeout, int nanos) -- 讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量”,當前線程被喚醒(進入“就緒狀態”)。
更詳細參考:http://www.cnblogs.com/skywang12345/p/3479224.html
五、線程的同步
同步目的:並發 多個線程訪問同一份資源 確保資源安全 -->線程安全
1.同步代碼塊
在代碼塊上加上”synchronized”關鍵字,則此代碼塊就稱為同步代碼塊
2.同步代碼塊格式
synchronized(同步對象){ 需要同步的代碼塊; }
3.同步方法
除了代碼塊可以同步,方法也是可以同步的
4.方法同步格式
synchronized void 方法名稱(){}
死鎖: 過多的同步容易造成死鎖
注:synchronized后續會單獨來學習
六、生產者和消費者模式
生產/消費者問題是個非常典型的多線程問題,涉及到的對象包括“生產者”、“消費者”、“倉庫”和“產品”。他們之間的關系如下:
(01) 生產者僅僅在倉儲未滿時候生產,倉滿則停止生產。
(02) 消費者僅僅在倉儲有產品時候才能消費,倉空則等待。
(03) 當消費者發現倉儲沒產品可消費時候會通知生產者生產。
(04) 生產者在生產出可消費產品時候,應該通知等待的消費者去消費。
簡單實例:
/** 一個場景,共同的資源 生產者消費者模式 信號燈法 wait() :等待,釋放鎖 sleep 不釋放鎖 notify()/notifyAll():喚醒 與 synchronized * @author Administrator * */ public class Movie { private String pic ; //信號燈 //flag -->T 生產生產,消費者等待 ,生產完成后通知消費 //flag -->F 消費者消費 生產者等待, 消費完成后通知生產 private boolean flag =true; /** * 播放 * @param pic */ public synchronized void play(String pic){ if(!flag){ //生產者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //開始生產 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("生產了:"+pic); //生產完畢 this.pic =pic; //通知消費 this.notify(); //生產者停下 this.flag =false; } public synchronized void watch(){ if(flag){ //消費者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //開始消費 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消費了"+pic); //消費完畢 //通知生產 this.notifyAll(); //消費停止 this.flag=true; } }
/** * 生產者 * @author Administrator * */ public class Player implements Runnable { private Movie m ; public Player(Movie m) { super(); this.m = m; } @Override public void run() { for(int i=0;i<20;i++){ if(0==i%2){ m.play("左青龍"); }else{ m.play("右白虎"); } } } }
public class Watcher implements Runnable { private Movie m ; public Watcher(Movie m) { super(); this.m = m; } @Override public void run() { for(int i=0;i<20;i++){ m.watch(); } } }
public class App { public static void main(String[] args) { //共同的資源 Movie m = new Movie(); //多線程 Player p = new Player(m); Watcher w = new Watcher(m); new Thread(p).start(); new Thread(w).start(); } }
上面采用的是信號燈法,即標志位來控制,那么下面這個實例則是通過倉庫來控制:
public class TestProduce { public static void main(String[] args) { SyncStack sStack = new SyncStack(); Shengchan sc = new Shengchan(sStack); Xiaofei xf = new Xiaofei(sStack); sc.start(); xf.start(); } } class Mantou { int id; Mantou(int id){ this.id=id; } } class SyncStack{ int index=0; Mantou[] ms = new Mantou[10]; public synchronized void push(Mantou m){ while(index==ms.length){ try { this.wait(); //wait后,線程會將持有的鎖釋放。sleep是即使睡着也持有互斥鎖。 } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); //喚醒在當前對象等待池中等待的第一個線程。notifyAll叫醒所有在當前對象等待池中等待的所有線程。 //如果不喚醒的話。以后這兩個線程都會進入等待線程,沒有人喚醒。 ms[index]=m; index++; } public synchronized Mantou pop(){ while(index==0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); index--; return ms[index]; } } class Shengchan extends Thread{ SyncStack ss = null; public Shengchan(SyncStack ss) { this.ss=ss; } @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("造饅頭:"+i); Mantou m = new Mantou(i); ss.push(m); } } } class Xiaofei extends Thread{ SyncStack ss = null; public Xiaofei(SyncStack ss) { this.ss=ss; } @Override public void run() { for (int i = 0; i < 20; i++) { Mantou m = ss.pop(); System.out.println("吃饅頭:"+i); } } }
七、面試題
線程和進程有什么區別?
答:一個進程是一個獨立(self contained)的運行環境,它可以被看作一個程序或者一個應用。而線程是在進程中執行的一個任務。線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。
如何在Java中實現線程?
答:
創建線程有兩種方式:
一、繼承 Thread 類,擴展線程。
二、實現 Runnable 接口。
啟動一個線程是調用run()還是start()方法?
答:啟動一個線程是調用start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由JVM 調度並執行,這並不意味着線程就會立即運行。run()方法是線程啟動后要進行回調(callback)的方法。
Thread類的sleep()方法和對象的wait()方法都可以讓線程暫停執行,它們有什么區別?
答:sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結束后會自動恢復(線程回到就緒狀態,請參考第66題中的線程狀態轉換圖)。wait()是Object類的方法,調用對象的wait()方法導致當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態。
線程的sleep()方法和yield()方法有什么區別?
答:
① sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
② 線程執行sleep()方法后轉入阻塞(blocked)狀態,而執行yield()方法后轉入就緒(ready)狀態;
③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
④ sleep()方法比yield()方法(跟操作系統CPU調度相關)具有更好的可移植性。
請說出與線程同步以及線程調度相關的方法。
答:
wait():使一個線程處於等待(阻塞)狀態,並且釋放所持有的對象的鎖;
sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理InterruptedException異常;
notify():喚醒一個處於等待狀態的線程,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且與優先級無關;
notityAll():喚醒所有處於等待狀態的線程,該方法並不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態;
八、總結
重點:1)線程的創建 2)線程的終止方法 3)線程的同步 4)生產者消費
參考資料:
裴新《多線程視頻》