# java線程詳解 ## 線程 ### 概念 說到線程,就不得不提進程,為什麼呢,因為進程是操作系統進行分配資源和調度的最小單位,比如windows系統安裝的應用軟體(office、qq、微信等)啟動時,由操作系統協調分配資源和調度執行稱之為一個進程,進程間是相互獨立和隔離的。而線程是進程最小執行 ...
java線程詳解
線程
概念
說到線程,就不得不提進程,為什麼呢,因為進程是操作系統進行分配資源和調度的最小單位,比如windows系統安裝的應用軟體(office、qq、微信等)啟動時,由操作系統協調分配資源和調度執行稱之為一個進程,進程間是相互獨立和隔離的。而線程是進程最小執行單位,一個進程的執行過程中可以有多個線程,這樣可以發揮多核CPU的能力,提高執行效率。
java中的線程不是由操作系統直接調度,而且通過java虛擬機與操作系統進行指令交互完成。所以對於java程式員來說,使用線程非常簡單,只需要在語言層面編寫完代碼,交給虛擬機運行,剩下的臟活累活在底層就由java虛擬機完成,使用線程一時爽,一直使用一直爽(哈哈,雖然多線程能充分壓榨CPU,但是用不好的話也會產生許多問題,比如併發導致的數據錯誤、系統負載飆升等)。
基本用法
有兩種方式來創建線程
1、一種是實現Runnable
介面,然後利用Thread
類的構造函數傳入Runnable介面創建Thread
實例。
2、另外一種是繼承Thread
。
1、實現Runnable
class WorkerThread1 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "執行完成");
}
}
2、繼承Thread
class WorkerThread2 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "執行完成");
}
}
測試用例:
public static void main(String[] args) {
test1();
}
private static void test1() {
new Thread(new WorkerThread1(), "t1").start();
Thread t2 = new WorkerThread2();
t2.setName("t2");
t2.start();
}
輸出:
註意這裡的執行順序不一定是t1先輸出,也可能是t2先輸出,因為線程啟動後只是準備就緒,最終需要等待操作系統調度執行才能執行。
t1執行完成
t2執行完成
這兩種方式都可以創建線程並執行,細心的讀者可能看過源碼發現其實Thread
類也實現了Runnable
介面。如果自己的類已經繼承了別的類,那麼可以實現Runnable
介面創建線程,否則可以繼承Thread
類覆寫run()
方法即可。
註意:要讓線程執行需要調用
start()
方法,這樣虛擬機才能創建一個線程等待操作系統調度,直接執行run()
方法則是在當前線程直接調用該方法,同步執行,不會再創建線程。
線程狀態及流轉
線程的生命周期可用狀態表示,總共有6種狀態,
Thread
類的源碼里我們可以看到有個枚舉類State
。
public enum State {
// 新建狀態,被new出來後,還未調用start方法
NEW,
// 可運行狀態,線程已就緒,獲取到CPU資源就運行,運行中就是Running狀態
RUNNABLE,
// 阻塞狀態,比如在等待鎖對象,或者讀取流等待
BLOCKED,
// 等待狀態,比如在等待鎖對象,需要被notify喚醒處於就緒狀態,獲取到CPU資源就運行
WAITING,
// 帶有超時時間的等待,時間過後自動返回繼續執行,比如sleep或者wait(long time)
TIMED_WAITING,
// 終止狀態,自然停止或者拋出異常停止
TERMINATED;
}
狀態流轉圖:
屬性及方法
屬性
常用並且需要關註的屬性如下:
private int priority;// 線程優先順序,1<=priority<=10,優先順序越大可能最終被操作系統優先調度的幾率越大,但不一定會被先調用
private boolean daemon = false;// 是否後臺線程,當所有的非後臺線程結束時,所有的後臺線程才結束,比如垃圾回收線程就是後臺線程,負責在程式運行過程中,清理掉不在使用的對象
private Runnable target;// 目標類,要使用線程執行的業務邏輯寫在run()方法里
private ThreadGroup group;// 線程組,所有的線程都必須屬於某一個線程組,預設的是父線程組,未創建線程組時即main線程組
ThreadLocal.ThreadLocalMap threadLocals = null;// 線程獨享本地變數表
ThreadLocal
不瞭解的童鞋可以參考ThreadLocal源碼分析
構造方法
其實最終都會調到一個init
的方法,這裡會初始化一些線程的基本信息。
ThreadGroup g:屬於的線程組(每個線程必須屬於一個線程組,沒有設置線程組的話這裡會預設使用父線程組,即main線程組)
Runnable target:要執行的方法
String name:線程名字,可單獨設置,預設使用的是`Thread-自增序號`
long stackSize:這隻線程占用的棧大小,一般不設置,由java虛擬機決定
AccessControlContext acc:這是一種安全訪問機制,一般不用自己設置。
常用方法
// jvm方法,獲取當前正在執行的線程
public static native Thread currentThread();
// 調用該方法所在的線程通知讓出CPU資源,由操作系統重新分配線程執行,有可能還是該線程繼續執行
public static native void yield();
// 線程休眠millis毫秒,讓出CPU資源,但是不會釋放持有的對象鎖,當被中斷時拋出異常
public static native void sleep(long millis) throws InterruptedException;
// 啟動線程,更合理的說法是線程準備就緒,等待最終操作系統調度執行
public synchronized void start()
// 中斷線程,其實只是設置中斷標誌為true,如果線程已經終止,調用該方法不會有任何作用,線程自身和安全驗證通過的線程才有許可權調該線程的這個方法,否則會返回異常SecurityException,還有以下幾種特殊場景。
// 1、如果當前線程處理wait()、join()、sleep(long)方法時中斷標誌將被清除,並且拋出異常InterruptedException。(清除中斷標誌是因為拋出了異常後可由程式控制執行,中斷標誌沒啥意義了)
// 2、繼承了InterruptibleChannel如果IO阻塞狀態,則channel會關閉並且中斷標誌設置為true,拋出異常ClosedByInterruptException
// 3、如果該線程阻塞在Selector,則中斷標誌設置為true並且select會立即返回一個非0的值表示select方法執行了
public void interrupt()
// 返回當前線程是否處於中斷狀態,並清除當前運行的線程的中斷標誌。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 返回當前線程是否處於中斷狀態,不會清除當前運行的線程的中斷標誌。線程終止後該方法始終返回false
public boolean isInterrupted() {
return isInterrupted(false);
}
常用場景
重點看下interrupt()
、interrupted()
、isInterrupted()
這三個方法的使用。
1、不使用變數終止線程
下麵示例調用了interrupt()
方法
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + ":" + System.currentTimeMillis());
}
}
}, "t1");
t.start();
//主線程休2秒
Thread.sleep(2000);
t.interrupt();
System.out.println(Thread.currentThread().getName() + "運行結束");
}
}
程式運行部分結果
t1:1584607075440
t1:1584607075440
t1:1584607075440
t1:1584607075440
main運行結束
t1:1584607077740
t1:1584607077740
t1:1584607077740
t1:1584607077740
t1:1584607077741
t1:1584607077741
t1:1584607077741
這裡我們看到調用了interrupt()
方法,但是t線程並沒有終止,因為這個方法只是設置中斷標誌為true,線程是否結束跟這個標誌沒有關係,需要其他邏輯判斷。
如果我們把程式的while迴圈的條件調整如下這樣,那麼t線程調用isInterrupted()
方法獲取到中斷標誌為true,!true條件結果為false所以while迴圈結束,線程自然終止。
while (true)
改成
while (!Thread.currentThread().isInterrupted())
2、線程組的操作
線程組主要是可以管理線程或線程組,一個大型的線程組看起來像是一棵樹,可方便的對線程和子線程組進行監控和操作,比如統計活躍線程和線程組,或者中斷和銷毀線程組操作等。
public static void main(String[] args) {
ThreadGroup tg1 = new ThreadGroup("tg1");
ThreadGroup tg2 = new ThreadGroup("tg2");
ThreadGroup tg3 = new ThreadGroup(tg2, "tg3");
Thread t1 = new WorkerThread(tg1, "t1");
Thread t2 = new WorkerThread(tg2, "t2");
Thread t3 = new WorkerThread(tg1, "t3");
Thread t4 = new WorkerThread(tg3, "t4");
t1.start();
t2.start();
t3.start();
t4.start();
System.out.println("tgName:" + tg1.getName() + "\tactiveCount:" + tg1.activeCount());
System.out.println("tgName:" + tg2.getName() + "\tactiveGroupCount:" + tg2.activeGroupCount());
System.out.println("tgName:" + tg3.getParent().getName() + "\tactiveGroupCount:" + tg3.activeGroupCount());
}
class WorkerThread extends Thread {
public WorkerThread(ThreadGroup tg, String name) {
super(tg, name);
}
@Override
public void run() {
while (true) {
}
}
}
輸出結果如下:
tgName:tg1 activeCount:2
tgName:tg2 activeGroupCount:1
tgName:tg2 activeGroupCount:0
總結
線程的使用可以提高系統的處理能力,比較複雜的業務可以使用多線程協作縮短執行時間,線程的結束不能僅僅依靠中斷,需要根據業務實現自己的邏輯。另外引入多線程會增加系統的複雜度,線程的管理問題也會比較麻煩,同時線程數過多會頻繁的讓CPU切換,增加系統負載,這種時候可以選擇使用線程池來解決問題,後續會再詳細介紹。