Java多執行緒詳解-入門篇
程式與執行緒
在講多執行緒之前,我覺得有必要先說一下程式與執行緒之間的關係與差異。
1、程式是資源分配的最小單位,執行緒是程式執行的最小單位(資源排程的最小單位);
2、程式有自己的獨立地址空間,每啟動一個程式,系統就會為它分配地址空間,建立資料表來維護程式碼段、堆疊段和資料段,這種操作非常昂貴;
而執行緒是共享程式中的資料的,使用相同的地址空間,因此CPU切換一個執行緒的花費遠比程式要小很多,同時建立一個執行緒的開銷也比程式要小很多;
3、執行緒之間的通訊更方便,同一程式下的執行緒共享全域性變數、靜態變數等資料,而程式之間的通訊需要以通訊的方式(IPC)進行。不過如何處理好同步與互斥是編寫多執行緒程式的難點;
4、但是多程式程式更健壯,多執行緒程式只要有一個執行緒死掉,整個程式也死掉了,而一個程式死掉並不會對另外一個程式造成影響,因為程式有自己獨立的地址空間。
通俗點來講,程式就像是工作管理員中的qq,chrome,網易雲音樂這種一個個應用,而執行緒就像是在這個程式中間的一次任務,比如你點選切換音樂,聊天傳送資訊等。
多執行緒的實現
在Java中多執行緒的實現有三種形式,這裡只說前兩種,繼承Thread類和實現Runnable介面。
1 繼承Thread類
//繼承Thread實現多執行緒
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "執行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
}
}
複製程式碼
上面這個兩個類,Thread1
類繼承了Thread父類,並重寫了裡面的run
方法。實現了多執行緒裡面的方法,並在main函式中進行例項化了兩個mTh1
,mTh2
兩個執行緒。
啟動main函式:
輸出:
A執行 : 0
B執行 : 0
A執行 : 1
A執行 : 2
A執行 : 3
A執行 : 4
B執行 : 1
B執行 : 2
B執行 : 3
B執行 : 4
複製程式碼
再執行一下:
A執行 : 0
B執行 : 0
B執行 : 1
B執行 : 2
B執行 : 3
B執行 : 4
A執行 : 1
A執行 : 2
A執行 : 3
A執行 : 4
複製程式碼
可以看到兩次執行的結果是不太一樣的。
說明
程式在啟動main函式時,Java虛擬機器器就已經啟動了一個主執行緒來執行main函式,在呼叫到
mTh1
,mTh2
的start方法時,就相當於有三個執行緒在同時工作了,這就是多執行緒的模式,進入了mTh1
子執行緒,這個執行緒中的操作,在這個執行緒中有sleep()
方法,Thread.sleep()方法呼叫目的是不讓當前執行緒獨自霸佔該程式所獲取的CPU資源,以留出一定時間給其他執行緒執行的機會。
實際上所有的執行緒執行順序都是不確定的,CPU資源的獲取完全是看兩個執行緒之間誰先搶佔上誰就先執行,當mTh1
搶佔上執行緒後,執行run
方法中的程式碼,到sleep()
方法進入休眠狀態,也就是阻塞狀態,然後CPU
資源會被釋放,A
,B
再次進行搶佔CPU
資源操作,搶佔上的繼續執行。在執行的結果中你也可以看到這個現象。
注意:
一個例項的start()方法不能重複呼叫,否則會出現java.lang.IllegalThreadStateException
異常。
2 實現java.lang.Runnable
介面
採用Runnable也是非常常見的一種,我們只需要重寫run方法即可。下面也來看個例項。
class Thread2 implements Runnable{
private String name;
public Thread2(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "執行 : " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
new Thread(new Thread2("C")).start();
new Thread(new Thread2("D")).start();
}
}
複製程式碼
整體和繼承Thread差別不大,因為在Thread類中也是繼承的Runnable介面。
輸出執行:
C執行 : 0
D執行 : 0
D執行 : 1
C執行 : 1
D執行 : 2
C執行 : 2
D執行 : 3
C執行 : 3
D執行 : 4
C執行 : 4
複製程式碼
說明:
Thread2
類通過實現Runnable
介面,使得該類有了多執行緒類的特徵。run()
方法是多執行緒程式的一個約定。所有的多執行緒程式碼都在run
方法裡面。Thread
類實際上也是實現了Runnable
介面的類。在啟動的多執行緒的時候,需要先通過
Thread
類的構造方法Thread(Runnable target)
構造出物件,然後呼叫Thread物件的start()方法來執行多執行緒程式碼。實際上所有的多執行緒程式碼都是通過執行
Thread
的start()
方法來執行的。因此,不管是擴充套件Thread類還是實現Runnable
介面來實現多執行緒,最終還是通過Thread
的物件的API
來控制執行緒的,熟悉Thread
類的API
是進行多執行緒程式設計的基礎。
Thread類和Runnable介面的區別
如果一個類繼承Thread
,則不適合資源共享。但是如果實現了Runable
介面的話,則很容易的實現資源共享。
總結:
實現Runnable
介面比繼承Thread
類所具有的優勢:
1):適合多個相同的程式程式碼的執行緒去處理同一個資源
2):可以避免java
中的單繼承的限制
3):增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立
4):執行緒池只能放入實現Runable
或callable
類執行緒,不能直接放入繼承Thread
的類
提醒一下大家:main方法其實也是一個執行緒。在java
中所以的執行緒都是同時啟動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。
在java
中,每次程式執行至少啟動2個執行緒。一個是main
執行緒,一個是垃圾收集執行緒。因為每當使用java
命令執行一個類的時候,實際上都會啟動一個JVM
,每一個JVM
實習在就是在作業系統中啟動了一個程式。
執行緒的狀態
下面先放一張執行緒的展示圖
1:新建狀態(New):new Thread(),新建立了一個執行緒;
2:就緒狀態(Runnable):新建完成後,主執行緒(main()方法)呼叫了該執行緒的start()方法,CPU目前在執行其他任務或者執行緒,這個建立好的執行緒就會進入就緒狀態,等待CPU資源執行程式,在執行之前的這段時間處於就緒狀態;
3:執行狀態(Running):字面意思,執行緒呼叫了start()方法之後並且搶佔到了CPU資源,執行run方法中的程式程式碼;
4:阻塞狀態(Blocked):阻塞狀態時執行緒在執行過程中因為某些操作暫停執行,放棄CPU使用權,進入就緒狀態和其他執行緒一同進行下次CPU資源的搶佔。
當發生如下情況時,執行緒將會進入阻塞狀態
① 執行緒呼叫sleep()方法主動放棄所佔用的處理器資源
② 執行緒呼叫了一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞
③ 執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有。關於同步監視器的知識、後面將有深入的介紹
④ 執行緒在等待某個通知(notify)
⑤ 程式呼叫了執行緒的suspend()方法將該執行緒掛起。但這個方法容易導致死鎖,所以應該儘量避免使用該方法
當前正在執行的執行緒被阻塞之後,其他執行緒就可以獲得執行的機會。被阻塞的執行緒會在合適的時候重新進入就緒狀態,注意是就緒狀態而不是執行狀態。也就是說,被阻塞執行緒的阻塞解除後,必須重新等待執行緒排程器再次排程它。
解除阻塞
針對上面幾種情況,當發生如下特定的情況時可以解除上面的阻塞,讓該執行緒重新進入就緒狀態:
① 呼叫sleep()方法的執行緒經過了指定時間。
② 執行緒呼叫的阻塞式IO方法已經返回。
③ 執行緒成功地獲得了試圖取得的同步監視器。
④ 執行緒正在等待某個通知時,其他執行緒發出了個通知。
⑤ 處於掛起狀態的執行緒被調甩了resdme()
恢復方法(會導致死鎖,儘量避免使用)。
5:死亡狀態(Dead):執行緒程式執行完成或者因為發生異常跳出了run()方法,執行緒生命週期結束。
執行緒的排程
1:調整執行緒優先順序:Java
執行緒有優先順序,優先順序高的執行緒會獲得較多的執行機會。
Java
執行緒的優先順序用整數表示,取值範圍是1~10,Thread
類有以下三個靜態常量:
static int MAX_PRIORITY
執行緒可以具有的最高優先順序,取值為10。
static int MIN_PRIORITY
執行緒可以具有的最低優先順序,取值為1。
static int NORM_PRIORITY
分配給執行緒的預設優先順序,取值為5。
Thread
類的setPriority()
和getPriority()
方法分別用來設定和獲取執行緒的優先順序。
每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY
。
執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。
JVM
提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread
類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式。
2、執行緒睡眠:Thread.sleep(long millis)
方法,使執行緒轉到阻塞狀態。millis
引數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable
)狀態。sleep()
平臺移植性好。
3、執行緒等待:Object
類中的wait()
方法,導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()
方法或notifyAll()
喚醒方法。這個兩個喚醒方法也是Object
類中的方法,行為等價於呼叫 wait(0) 一樣。
4、執行緒讓步:Thread.yield()
方法,暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更高優先順序的執行緒。
5、執行緒加入:join()
方法,等待其他執行緒終止。在當前執行緒中呼叫另一個執行緒的join()
方法,則當前執行緒轉入阻塞狀態,直到另一個程式執行結束,當前執行緒再由阻塞轉為就緒狀態。
6、**執行緒喚醒:**Object類中的notify()
方法,喚醒在此物件監視器上等待的單個執行緒。如果所有執行緒都在此物件上等待,則會選擇喚醒其中一個執行緒。選擇是任意性的,並在對實現做出決定時發生。執行緒通過呼叫其中一個 wait 方法,在物件的監視器上等待。 直到當前的執行緒放棄此物件上的鎖定,才能繼續執行被喚醒的執行緒。被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒進行競爭;例如,喚醒的執行緒在作為鎖定此物件的下一個執行緒方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll()
,喚醒在此物件監視器上等待的所有執行緒。
注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,不再介紹。因為有死鎖傾向。
常用函式說明
1:sleep(long millis)
: 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行);
sleep()使當前執行緒進入停滯狀態(阻塞當前執行緒),讓出CUP的使用、目的是不讓當前執行緒獨自霸佔該程式所獲的CPU資源,以留一定時間給其他執行緒執行的機會;
sleep()是Thread類的Static(靜態)的方法;因此他不能改變物件的機鎖,所以當在一個Synchronized塊中呼叫Sleep()方法是,執行緒雖然休眠了,但是物件的機鎖並木有被釋放,其他執行緒無法訪問這個物件(即使睡著也持有物件鎖)。
在sleep()休眠時間期滿後,該執行緒不一定會立即執行,這是因為其它執行緒可能正在執行而且沒有被排程為放棄執行,除非此執行緒具有更高的優先順序。
2:join()
:指等待t執行緒終止。
Thread t = new AThread(); t.start(); t.join();
複製程式碼
為什麼要用join()方法
在很多情況下,主執行緒生成並起動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之後再結束,這個時候就要用到join()方法了。
不加join()
方法:
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
super(name);
this.name=name;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " 執行緒執行開始!");
for (int i = 0; i < 5; i++) {
System.out.println("子執行緒"+name + "執行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 執行緒執行結束!");
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主執行緒執行開始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
System.out.println(Thread.currentThread().getName()+ "主執行緒執行結束!");
}
}
複製程式碼
輸出結果:
main主執行緒執行開始!
main主執行緒執行結束!
B 執行緒執行開始!
子執行緒B執行 : 0
A 執行緒執行開始!
子執行緒A執行 : 0
子執行緒B執行 : 1
子執行緒A執行 : 1
子執行緒A執行 : 2
子執行緒A執行 : 3
子執行緒A執行 : 4
A 執行緒執行結束!
子執行緒B執行 : 2
子執行緒B執行 : 3
子執行緒B執行 : 4
B 執行緒執行結束!
複製程式碼
發現了main
函式主執行緒比A,B子執行緒都提前結束。
加入join()
方法:
(執行緒方法一致,不再重複)
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主執行緒執行開始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主執行緒執行結束!");
}
}
複製程式碼
執行結果:
main主執行緒執行開始!
A 執行緒執行開始!
子執行緒A執行 : 0
B 執行緒執行開始!
子執行緒B執行 : 0
子執行緒A執行 : 1
子執行緒B執行 : 1
子執行緒A執行 : 2
子執行緒B執行 : 2
子執行緒A執行 : 3
子執行緒B執行 : 3
子執行緒A執行 : 4
子執行緒B執行 : 4
A 執行緒執行結束!
main主執行緒執行結束!
複製程式碼
主執行緒一定會等子執行緒都結束了才結束
3:yield():
暫停當前正在執行的執行緒物件,並執行其他執行緒。
Thread.yield()
方法作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。
yield()
應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()
達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
結論:yield()
從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()
將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。可看上面的圖。
class ThreadYield extends Thread{
public ThreadYield(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 當i為30時,該執行緒就會把CPU時間讓掉,讓其他或者自己的執行緒執行(也就是誰先搶到誰執行)
if (i ==30) {
this.yield();
}
}
}
}
public class Main {
public static void main(String[] args) {
ThreadYield yt1 = new ThreadYield("張三");
ThreadYield yt2 = new ThreadYield("李四");
yt1.start();
yt2.start();
}
}
複製程式碼
執行結果:
第一種情況:李四(執行緒)當執行到30時會CPU時間讓掉,這時張三(執行緒)搶到CPU時間並執行。
第二種情況:李四(執行緒)當執行到30時會CPU時間讓掉,這時李四(執行緒)搶到CPU時間並執行。
sleep()和yield()的區別
sleep()使當前執行緒進入停滯狀態,確切來說進入阻塞狀態,等sleep()規定的時間過了之後,該執行緒會繼續執行,而停滯時間內會執行其他執行緒,yield()方法是直接停止該執行緒然後讓執行緒從執行狀態變成就緒狀態,跟其他執行緒一塊去搶奪CPU資源,有可能他會立即又搶奪到CPU資源,繼續執行執行緒。
sleep 方法使當前執行中的執行緒睡眠一段時間,進入不可執行狀態,這段時間的長短是由程式設定的,yield 方法使當前執行緒讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先順序的執行緒處於同可執行狀態,如有,則把 CPU 的佔有權交給此執行緒,否則,繼續執行原來的執行緒。所以yield()方法稱為“退讓”,它把執行機會讓給了同等優先順序的其他執行緒。
另外,sleep 方法允許較低優先順序的執行緒獲得執行機會,但 yield() 方法執行時,當前執行緒仍處在可執行狀態,所以,不可能讓出較低優先順序的執行緒些時獲得 CPU 佔有權。在一個執行系統中,如果較高優先順序的執行緒沒有呼叫 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先順序執行緒只能等待所有較高優先順序的執行緒執行結束,才有機會執行。
4:setPriority()
: 更改執行緒的優先順序。
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10
5:interrupt()
:
interrupt()
方法不是中斷某個執行緒,而是給執行緒傳送一箇中斷訊號,讓執行緒在無限等待時(如死鎖時)能丟擲異常,從而結束執行緒,但是如果你吃掉了這個異常,那麼這個執行緒還是不會中斷的!
(中斷這塊我會專門寫一篇來講interrupt,isInterrupted,interrupted
。還有已經被淘汰的stop,suspend
方法為什麼會被淘汰)
6:其他方法
還有wait(),notify(),notifyAll()
這些方法,因為這三個方法要跟執行緒的鎖結合起來講解,所以我們放在下次跟多執行緒的鎖一塊講解。還有就是Java執行緒池的概念以及鎖中的區別等等。
執行緒資料傳遞
在傳統的同步開發模式下,當我們呼叫一個函式時,通過這個函式的引數將資料傳入,並通過這個函式的返回值來返回最終的計算結果。但在多執行緒的非同步開發模式下,資料的傳遞和返回和同步開發模式有很大的區別。由於執行緒的執行和結束是不可預料的,因此,在傳遞和返回資料時就無法象函式一樣通過函式引數和return語句來返回資料。
1:通過構造方法傳遞資料
在建立執行緒時,必須要建立一個Thread類的或其子類的例項。因此,我們不難想到在呼叫start方法之前通過執行緒類的構造方法將資料傳入執行緒。並將傳入的資料使用類變數儲存起來,以便執行緒使用(其實就是在run方法中使用)。下面的程式碼演示瞭如何通過構造方法來傳遞資料:
package mythread;
public class MyThread1 extends Thread
{
private String name;
public MyThread1(String name)
{
this.name = name;
}
public void run()
{
System.out.println("hello " + name);
}
public static void main(String[] args)
{
Thread thread = new MyThread1("world");
thread.start();
}
}
複製程式碼
由於這種方法是在建立執行緒物件的同時傳遞資料的,因此,線上程執行之前這些資料就就已經到位了,這樣就不會造成資料線上程執行後才傳入的現象。如果要傳遞更復雜的資料,可以使用集合、類等資料結構。使用構造方法來傳遞資料雖然比較安全,但如果要傳遞的資料比較多時,就會造成很多不便。由於Java沒有預設引數,要想實現類似預設引數的效果,就得使用過載,這樣不但使構造方法本身過於複雜,又會使構造方法在數量上大增。因此,要想避免這種情況,就得通過類方法或類變數來傳遞資料。
2:通過變數和方法傳遞資料
向物件中傳入資料一般有兩次機會,第一次機會是在建立物件時通過構造方法將資料傳入,另外一次機會就是在類中定義一系列的public的方法或變數(也可稱之為欄位)。然後在建立完物件後,通過物件例項逐個賦值。下面的程式碼是對MyThread1類的改版,使用了一個setName方法來設定 name變數:
package mythread;
public class MyThread2 implements Runnable
{
private String name;
public void setName(String name)
{
this.name = name;
}
public void run()
{
System.out.println("hello " + name);
}
public static void main(String[] args)
{
MyThread2 myThread = new MyThread2();
myThread.setName("world");
Thread thread = new Thread(myThread);
thread.start();
}
}
複製程式碼
3:通過回撥函式傳遞資料
上面討論的兩種向執行緒中傳遞資料的方法是最常用的。但這兩種方法都是main方法中主動將資料傳入執行緒類的。這對於執行緒來說,是被動接收這些資料的。然而,在有些應用中需要線上程執行的過程中動態地獲取資料,如在下面程式碼的run方法中產生了3個隨機數,然後通過Work類的process方法求這三個隨機數的和,並通過Data類的value將結果返回。從這個例子可以看出,在返回value之前,必須要得到三個隨機數。也就是說,這個 value是無法事先就傳入執行緒類的。
package mythread;
class Data
{
public int value = 0;
}
class Work
{
public void process(Data data,Integer numbers)
{
for (int n : numbers)
{
data.value += n;
}
}
}
public class MyThread3 extends Thread
{
private Work work;
public MyThread3(Work work)
{
this.work = work;
}
public void run()
{
java.util.Random random = new java.util.Random();
Data data = new Data();
int n1 = random.nextInt(1000);
int n2 = random.nextInt(2000);
int n3 = random.nextInt(3000);
work.process(data,n1,n2,n3); // 使用回撥函式
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
+ String.valueOf(n3) + "=" + data.value);
}
public static void main(String[] args)
{
Thread thread = new MyThread3(new Work());
thread.start();
}
}
複製程式碼
總結
這篇基本講了Java多執行緒中的基礎部分,後續還會有執行緒同步(鎖),執行緒如何正確的中斷,執行緒池等。Java的多執行緒部分是比較複雜的,只有平時多看多練才能記住並應用到實際專案中去。互勉~