最近聽很多面試的小夥伴說,網上往往是一篇一篇的Java多執行緒的文章,除了書籍沒有什麼學習多執行緒的一系列文章。但是僅僅憑藉一兩篇文章很難對多執行緒有系統的學習,而且面試的時候多執行緒這方面的知識往往也是考察的重點,所以考慮之下決定寫一系列關於Java多執行緒的文章。文章參考了高老師的《Java多執行緒程式設計核心技術》。力爭使用最短的篇幅把Java多執行緒的知識作以系統的講述。
本節思維導圖:
思維導圖原始檔+思維導圖軟體關注微信公眾號:“Java面試通關手冊”回覆關鍵字:“Java多執行緒”免費領取。一 程式和多執行緒簡介
1.1 相關概念
何為執行緒?
執行緒與程式相似,但執行緒是一個比程式更小的執行單位。一個程式在其執行的過程中可以產生多個執行緒。與程式不同的是同類的多個執行緒共享同一塊記憶體空間和一組系統資源,所以系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程式小得多,也正因為如此,執行緒也被稱為輕量級程式。
何為程式?
程式是程式的一次執行過程,是系統執行程式的基本單位,因此程式是動態的。系統執行一個程式即是一個程式從建立,執行到消亡的過程。簡單來說,一個程式就是一個執行中的程式,它在計算機中一個指令接著一個指令地執行著,同時,每個程式還佔有某些系統資源如CPU時間,記憶體空間,檔案,檔案,輸入輸出裝置的使用權等等。換句話說,當程式在執行時,將會被作業系統載入記憶體中。
執行緒和程式有何不同?
執行緒是程式劃分成的更小的執行單位。執行緒和程式最大的不同在於基本上各程式是獨立的,而各執行緒則不一定,因為同一程式中的執行緒極有可能會相互影響。從另一角度來說,程式屬於作業系統的範疇,主要是同一段時間內,可以同時執行一個以上的程式,而執行緒則是在同一程式內幾乎同時執行一個以上的程式段。
1.2 多執行緒
何為多執行緒?
多執行緒就是幾乎同時執行多個執行緒(一個處理器在某一個時間點上永遠都只能是一個執行緒!即使這個處理器是多核的,除非有多個處理器才能實現多個執行緒同時執行。)。幾乎同時是因為實際上多執行緒程式中的多個執行緒實際上是一個執行緒執行一會然後其他的執行緒再執行,並不是很多書籍所謂的同時執行。
為什麼多執行緒是必要的?
- 使用執行緒可以把佔據長時間的程式中的任務放到後臺去處理
- 使用者介面可以更加吸引人,這樣比如使用者點選了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度
- 程式的執行速度可能加快
二 使用多執行緒
2.1繼承Thread類
MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();
System.out.println("執行結束");
}
}
複製程式碼
執行結果:
從上面的執行結果可以看出:執行緒是一個子任務,CPU以不確定的方式,或者說是以隨機的時間來呼叫執行緒中的run方法。2.2實現Runnable介面
推薦實現Runnable介面方式開發多執行緒,因為Java單繼承但是可以實現多個介面。
MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable");
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
System.out.println("執行結束!");
}
}
複製程式碼
執行結果:
三 例項變數和執行緒安全
定義執行緒類中的例項變數針對其他執行緒可以有共享和不共享之分
3.1 不共享資料的情況
MyThread.java
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由 " + MyThread.currentThread().getName()
+ " 計算,count=" + count);
}
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}
複製程式碼
執行結果:
可以看出每個執行緒都有一個屬於自己的例項變數count,它們之間互不影響。我們再來看看另一種情況。3.2 共享資料的情況
MyThread.java
public class MyThread extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
count--;
System.out.println("由 " + MyThread.currentThread().getName() + " 計算,count=" + count);
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
MyThread mythread=new MyThread();
//下列執行緒都是通過mythread物件建立的
Thread a=new Thread(mythread,"A");
Thread b=new Thread(mythread,"B");
Thread c=new Thread(mythread,"C");
Thread d=new Thread(mythread,"D");
Thread e=new Thread(mythread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
複製程式碼
執行結果:
可以看出這裡已經出現了錯誤,我們想要的是依次遞減的結果。為什麼呢??因為在大多數jvm中,count--的操作分為如下下三步:
- 取得原有count值
- 計算i -1
- 對i進行賦值
所以多個執行緒同時訪問時出現問題就是難以避免的了。
那麼有沒有什麼解決辦法呢?
答案是:當然有,而且很簡單。
在run方法前加上synchronized關鍵字即可得到正確答案。
加上關鍵字後的執行結果:
四 一些常用方法
4.1 currentThread()
返回對當前正在執行的執行緒物件的引用。
4.2 getId()
返回此執行緒的識別符號
4.3 getName()
返回此執行緒的名稱
4.4 getPriority()
返回此執行緒的優先順序
4.5 isAlive()
測試這個執行緒是否還處於活動狀態。
什麼是活動狀態呢?
活動狀態就是執行緒已經啟動且尚未終止。執行緒處於正在執行或準備執行的狀態。
4.6 sleep(long millis)
使當前正在執行的執行緒以指定的毫秒數“休眠”(暫時停止執行),具體取決於系統定時器和排程程式的精度和準確性。
4.7 interrupt()
中斷這個執行緒。
4.8 interrupted() 和isInterrupted()
interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有將狀態標誌清除為false的功能
isInterrupted(): 測試執行緒Thread對相關是否已經是中斷狀態,但部清楚狀態標誌
4.9 setName(String name)
將此執行緒的名稱更改為等於引數 name 。
4.10 isDaemon()
測試這個執行緒是否是守護執行緒。
4.11 setDaemon(boolean on)
將此執行緒標記為 daemon執行緒或使用者執行緒。
4.12 join()
在很多情況下,主執行緒生成並起動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之後再結束,這個時候就要用到join()方法了。
join()的作用是:“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行
4.13 yield()
yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU時間。注意:放棄的時間不確定,可能一會就會重新獲得CPU時間片。
4.14 setPriority(int newPriority)
更改此執行緒的優先順序
五 如何停止一個執行緒呢?
stop(),suspend(),resume()(僅用於與suspend()一起使用)這些方法已被棄用,所以我這裡不予講解。
5.1 使用interrupt()方法
我們上面提到了interrupt()方法,先來試一下interrupt()方法能不能停止執行緒 MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 5000000; i++) {
System.out.println("i=" + (i + 1));
}
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}
複製程式碼
執行上訴程式碼你會發現,執行緒並不會終止。
針對上面程式碼的一個改進:
interrupted()方法判斷執行緒是否停止,如果是停止狀態則break
MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已經是停止狀態了!我要退出了!");
break;
}
System.out.println("i=" + (i + 1));
}
System.out.println("看到這句話說明執行緒並未終止------");
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
}
}
複製程式碼
執行結果:
for迴圈雖然停止執行了,但是for迴圈下面的語句還是會執行,說明執行緒並未被停止。5.2 使用return停止執行緒
MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
while (true) {
if (this.isInterrupted()) {
System.out.println("ֹͣ停止了!");
return;
}
System.out.println("timer=" + System.currentTimeMillis());
}
}
}
複製程式碼
Run.java
public class Run {
public static void main(String[] args) throws InterruptedException {
MyThread t=new MyThread();
t.start();
Thread.sleep(2000);
t.interrupt();
}
}
複製程式碼
執行結果:
當然還有其他停止執行緒的方法,後面再做介紹。六 執行緒的優先順序
每個執行緒都具有各自的優先順序,執行緒的優先順序可以在程式中表明該執行緒的重要性,如果有很多執行緒處於就緒狀態,系統會根據優先順序來決定首先使哪個執行緒進入執行狀態。但這個並不意味著低 優先順序的執行緒得不到執行,而只是它執行的機率比較小,如垃圾回收機制執行緒的優先順序就比較低。所以很多垃圾得不到及時的回收處理。
執行緒優先順序具有繼承特性比如A執行緒啟動B執行緒,則B執行緒的優先順序和A是一樣的。
執行緒優先順序具有隨機性也就是說執行緒優先順序高的不一定每一次都先執行完。
Thread類中包含的成員變數代表了執行緒的某些優先順序。如Thread.MIN_PRIORITY(常數1),Thread.NORM_PRIORITY(常數5), Thread.MAX_PRIORITY(常數10)。其中每個執行緒的優先順序都在Thread.MIN_PRIORITY(常數1) 到Thread.MAX_PRIORITY(常數10) 之間,在預設情況下優先順序都是Thread.NORM_PRIORITY(常數5)。
學過作業系統這門課程的話,我們可以發現多執行緒優先順序或多或少借鑑了作業系統對程式的管理。
執行緒優先順序具有繼承特性測試程式碼:
MyThread1.java:
public class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("MyThread1 run priority=" + this.getPriority());
MyThread2 thread2 = new MyThread2();
thread2.start();
}
}
複製程式碼
MyThread2.java:
public class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("MyThread2 run priority=" + this.getPriority());
}
}
複製程式碼
Run.java:
public class Run {
public static void main(String[] args) {
System.out.println("main thread begin priority="
+ Thread.currentThread().getPriority());
Thread.currentThread().setPriority(6);
System.out.println("main thread end priority="
+ Thread.currentThread().getPriority());
MyThread1 thread1 = new MyThread1();
thread1.start();
}
}
複製程式碼
執行結果:
七 Java多執行緒分類
7.1 多執行緒分類
使用者執行緒:執行在前臺,執行具體的任務,如程式的主執行緒、連線網路的子執行緒等都是使用者執行緒
守護執行緒:執行在後臺,為其他前臺執行緒服務.也可以說守護執行緒是JVM中非守護執行緒的 “傭人”。
特點:一旦所有使用者執行緒都結束執行,守護執行緒會隨JVM一起結束工作
應用:資料庫連線池中的檢測執行緒,JVM虛擬機器啟動後的檢測執行緒
最常見的守護執行緒:垃圾回收執行緒
7.2 如何設定守護執行緒?
可以通過呼叫Thead類的setDaemon(true)方法設定當前的執行緒為守護執行緒
注意事項:
1. setDaemon(true)必須在start()方法前執行,否則會丟擲IllegalThreadStateException異常
2. 在守護執行緒中產生的新執行緒也是守護執行緒
3. 不是所有的任務都可以分配給守護執行緒來執行,比如讀寫操作或者計算邏輯
複製程式碼
MyThread.java:
public class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + (i));
Thread.sleep(100);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
複製程式碼
Run.java:
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我離開thread物件也不再列印了,也就是停止了!");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
複製程式碼
執行結果:
如果你覺得博主的文章不錯,歡迎轉發點贊。你能從中學到知識就是我最大的幸運。 歡迎關注我的微信公眾號:“Java面試通關手冊”(分享各種Java學習資源,面試題,以及企業級Java實戰專案回覆關鍵字免費領取):