Java 執行緒基礎
文章目錄
1 Java執行緒基礎
案例中涉及程式碼 --> 點選跳轉
1.1 並行、序列與併發
場景:餐廳後廚
序列:大廚做完乾煸豆角後,又做了一盤椒鹽蘑菇;
並行:大廚同時開了兩個灶,同時做乾煸豆角和椒鹽蘑菇;
併發:大廚和他的3個徒弟同時開了8個灶,做了4份乾煸豆角和椒鹽蘑菇;
1.2 執行緒生命週期
每個執行緒都有自己的區域性變量表、程式計數器和生命週期:
- NEW 新建
- RUNNABLE 可執行
- RUNNING 執行中
- BLOCKED 阻塞
- TERMINATED 結束
除了new和terminated,其他各個狀態之間會在不同的條件下會進行轉換:
1.2.1 NEW
new一個Thread例項時,進入NEW狀態,這時的thread例項只是一個簡單的物件:
Thread thread = new Thread();
1.2.2 RUNNABLE
start()方法讓一個thread例項進入RUNNABEL狀態,這時才真正在JVM中建立了一個執行緒,等待CPU的排程獲得執行權:
Thread thread = new Thread();
thread.start();
1.2.3 RUNNING
一旦CPU通過輪詢或其他方式從執行佇列中選中該執行緒,此時才會真正執行執行緒中的邏輯程式碼,進入RUNNING狀態;
1.2.4 BLCOKED
如下方法可以讓一個執行緒進入BLCOKED狀態:
Object的wait方法
Thread的sleep方法
Thread的join方法
InterruptibleChannel的io操作
Selector的wakeup方法
1.2.5 TERMINATED
ERMINATED狀態是一個現成的最終狀態,處於該狀態後執行緒生命週期也就結束了,不會在切換至其它狀態了;
1.3 執行緒執行單元
1.3.1 執行單元
每個執行緒的業務邏輯部分被稱為執行單元,而邏輯部分都是寫在run()方法中的,也就是說執行緒的執行單元就是run()方法;
***建立執行緒***有一種方式:構造Thread類;
***實現執行單元***有兩種方式:
1)重寫Thread類的run()方法 ;
public class ThreadChild extends Thread {
@Override
public void run() {
Thread.currentThread().setName("Thread's child");
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "------------");
}
}
2)實現Runnable介面的runnable方法;
public class RunnableImpl implements Runnable {
public void run() {
Thread.currentThread().setName("Runnable's implemention");
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "------------");
}
}
1.3.2 Thread中的run()方法–模板方法的應用
//new一個Thread物件
Thread thread = new Thread();
在直接new一個Thread物件的時候,run()方法中是一個空實現,看下Thread中的run()方法:
看13.1中的第一個例子,執行邏輯run()方法是在子類ThreadChild中實現了邏輯細節,這是一個典型的模板方法;
1.4 Thread&Runnabel的關係
1.4.1 類圖上的關係
Runnable現為一個函式式介面,Thread類是Runnable的一個實現類,實現了Runnable的run()方法,Thread不僅有run()方法,還有很多其他重要的方法來管理自己例項化執行緒的生命週期;
1.4.2 Runnable較Thread易於共享資源
案例為營業大廳取號機,從資源共享角度看通過實現Runnable介面更易於資源共享:
1)用Thread實現執行單元:
public class TicketWindow extends Thread {
//櫃檯名稱
private final String name;
//最多受理的業務數
private static final int max = 10;
private static int index = 1;
public TicketWindow(String name) {
this.name = name;
}
@Override
public void run() {
while (index < max)
System.out.println("櫃檯:" + name + "當前號碼為:" + (index++));
}
}
呼叫方:
public static void main(String[] args) {
TicketWindow ticketWindow1 = new TicketWindow("一號取號機");
TicketWindow ticketWindow2 = new TicketWindow("二號取號機");
TicketWindow ticketWindow3 = new TicketWindow("三號取號機");
//三個TicketWindow執行緒公用靜態MAX[10]
ticketWindow1.start();
ticketWindow2.start();
ticketWindow3.start();
}
執行結果:
櫃檯:一號取號機當前號碼為:1
櫃檯:一號取號機當前號碼為:2
櫃檯:一號取號機當前號碼為:3
櫃檯:一號取號機當前號碼為:4
櫃檯:一號取號機當前號碼為:5
櫃檯:一號取號機當前號碼為:6
櫃檯:一號取號機當前號碼為:7
櫃檯:一號取號機當前號碼為:8
櫃檯:一號取號機當前號碼為:9
1)用Runnable實現執行單元:
public class TicketRunnable implements Runnable {
private int index = 1;//不用static修改
private final static int MAX = 10;
public void run() {
while (index <= MAX) {
System.out.println(Thread.currentThread().getName() + "號碼是:" + (index++));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
呼叫方:
public static void main(String[] args) {
TicketRunnable ticketRunnable = new TicketRunnable();
Thread ticketWindow4 = new Thread(ticketRunnable,"四號取號機");
Thread ticketWindow5 = new Thread(ticketRunnable,"五號取號機");
Thread ticketWindow6 = new Thread(ticketRunnable,"六號取號機");
//三個執行緒公用一個runnable的私有屬性MAX[10]
ticketWindow4.start();
ticketWindow5.start();
ticketWindow6.start();
}
執行結果:
四號取號機號碼是:1
五號取號機號碼是:2
六號取號機號碼是:3
四號取號機號碼是:4
五號取號機號碼是:5
六號取號機號碼是:6
四號取號機號碼是:7
六號取號機號碼是:8
五號取號機號碼是:7
四號取號機號碼是:9
六號取號機號碼是:10
1.4.3 Thread&Runnabel的關係–策略模式的應用
1.4.3.1 策略模式
要了解Thread與Runnable的關係需要先了解策略模式:
策略模式:業務和行為解耦,職責分明,不同上下環境下執行不同的策略;
參見1–>菜鳥教程
參見2–>我的案例
1.4.3.2 Java執行緒策略模式的應用
上下文角色:Thread -->控制執行緒生命週期
策略角色:Runnable實現類 --> 不同的實現類是不同的策略,實現不同的邏輯
執行緒策略模式的應用 --> 不同的業務場景下生成不同業務邏輯的執行緒,業務邏輯和執行緒生命週期控制解構,Thread和Runnable職責分明:
public static void main(String[] args) {
//策略角色:不同的Runnable實現類
ThreeYearsOldRunnable threeYearsOldRunnable = new ThreeYearsOldRunnable();
TenYearsOldRunnable tenYearsOldRunnable = new TenYearsOldRunnable();
EighteenYearsOldRunnable eighteenYearsOldRunnable = new EighteenYearsOldRunnable();
TwentyFiveYearsOldRunnable twentyFiveYearsOldRunnable = new TwentyFiveYearsOldRunnable();
ThirtyYearsOldRunnable thirtyYearsOldRunnable = new ThirtyYearsOldRunnable();
//上下文角色:Thread例項
Thread threeYearsOld = new Thread(threeYearsOldRunnable, "三歲");
Thread tenYearsOld = new Thread(tenYearsOldRunnable, "十歲");
Thread eighteenYearsOld = new Thread(eighteenYearsOldRunnable, "十八歲");
Thread wentyFiveYearsOld = new Thread(twentyFiveYearsOldRunnable, "二十五歲");
Thread thirtyYearsOld = new Thread(thirtyYearsOldRunnable, "三十歲");
//不同的上下文下
for (int age = 0; age <= 100; age++) {
if (3 == age)
threeYearsOld.start();
if (10 == age)
tenYearsOld.start();
if (18 == age)
eighteenYearsOld.start();
if (25 == age)
wentyFiveYearsOld.start();
if (30 == age)
thirtyYearsOld.start();
}
}
執行結果:
三歲:叔叔好~我還有很多玩具要玩~
十歲:我在三年級學習,數學好難~
十八歲:告別難忘的高中時代,期待我的大學生活~
二十五歲:告別大學,初入職場處處碰壁~
三十歲: 我有了孩子,才知道我的爸媽多麼不容易~
1.4.4 Runnable&Thread關係總結
- Thread是Runnable的實現類,實現了Runnable的run()方法,但是個空方法,應用了模板模式,具體的實現交給子類;
- Thread和Runnable應用了策略模式,Thread管理執行緒的生命週期,Runnable實現類實現業務邏輯,兩者職責分明;
- 建立執行緒時Runnable實現類較Thread子類能更好更容易地線上程間共享資源;
2 Thread建構函式
從Thread構造方法來看Thread的名字、與ThreadGroup和Jvm Stack的關係;
2.1 執行緒命名
在常見執行緒的時候可以給執行緒指定一個名字,便於在多執行緒程式中查詢問題;
2.1.1 執行緒的預設命名
- Thread()
- Thread(Runnable Target)
- Thread(ThreadGroup group, Runnable Target)
這三個構造方法沒有提供執行緒命名的引數,執行緒會進行如下命名:以"Thread-"作為字首與一個自增數進行組合,自增數會隨著jvm程序中執行緒的數量不斷自增
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
啟了一個執行緒,被預設命名為"Thread-0":
可以線上程啟動前為其命名:
new Thread(() ->
{
//為其命名
Thread.currentThread().setName("weixx");
try {
TimeUnit.SECONDS.sleep(100l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
2.1.2 用帶有命名引數的構造方法為執行緒命名
- public Thread(String name)
- public Thread(ThreadGroup group, String name)
- public Thread(Runnable target, String name)
- public Thread(ThreadGroup group, Runnable target, String name)
- public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
2.2 ThreadGroup
- public Thread(ThreadGroup group, String name)
- public Thread(ThreadGroup group, Runnable target)
- public Thread(ThreadGroup group, Runnable target, String name)
- public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
在Thread構造方法中:
- 可以顯式地為執行緒指定group
- 如果沒有顯式地指定一個ThreadGroup,則會將其加入其父執行緒所在的執行緒組
建立兩個執行緒,一個構造時候指定ThreadGroup,一個不指定:
注:main方法的ThreadGroup為"main"
2.3 JVM Stack 虛擬機器棧
需要先了解JVM記憶體結構–>[importnew]
2.3.1 帶有stackSize引數的構造方法
- public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
執行緒建立時,除了這個構造方法顯式的指定了stackSize,其它構造方法都使用了預設值"0":
官方文件對stackSize的解釋:
- stackSize越大代表著當前執行緒內方法呼叫遞迴深度越深
- stackSize越小代表著建立執行緒的數量越多
當程式進行無限制深度遞迴時,Java棧中會不斷地進行壓棧彈棧操作,JVM的記憶體大小是有限的,終有被壓爆的時候,最後會丟擲StackOverFlowError異常,stackSize數量級大小與遞迴深度成正比,該引數一般不會主動設定,採用系統預設值"0"就好;
2.3.2 Java棧深度
棧幀結構圖:
每個執行緒建立的時候,JVM會為其建立Java棧,Java棧的大小可以通過-xss引數調整,方法呼叫就是棧幀被壓入和彈出的過程,由結構圖可以看出Java虛擬機器棧大小相同的情況下,區域性變量表等佔用的記憶體越小,可以被壓入的棧幀就越多,反之越少,棧幀的大小被稱為寬度,棧幀的數量被成為Java棧的深度;
2.3.3 Thread與Java棧
程序的大小 ≈ 堆記憶體 + 執行緒數量 * 棧記憶體
執行緒數量 = (最大地址空間(MaxProcessMemory) - JVM堆記憶體 - ReservedOsMemory)/ThreadStackSize(XSS)
結論:執行緒數量與Java棧記憶體大小成反比,與堆記憶體成反比
注:ReservedOsMemory為系統保留記憶體;
2.4 守護執行緒
守護執行緒是特殊的執行緒,一般用於處理後臺工作,如JDK垃圾回收;
2.4.1 JVM程式什麼情況下會退出
官方文件:若JVM中沒有一個非守護執行緒,則JVM的程序會退出;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(
() -> {
int i = 1;
try {
while (true) {
Thread.currentThread().setName("weixx");
//1當前執行緒sleep 10s
TimeUnit.SECONDS.sleep(10l);
System.out.println("thread 第[" + i + "]sleep結束~");
i++;
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("thread 掛了~");
}
}
);
//2 設定為守護執行緒
thread.setDaemon(true);
//3 啟動執行緒
thread.start();
//4 main方法執行緒sleep 5s
TimeUnit.SECONDS.sleep(5l);
System.out.println("main方法生命週期結束~");
}
發現main方法執行緒結束後,JVM也沒有退出,因為JVM程序中還存在一個非守護執行緒在執行;
現將2初的註釋開啟,將thread設定為守護執行緒,再次執行程式:
thread第一次sleep結束後,JVM直接退出了,連日誌都沒來得及列印,因為沒有一個非守護執行緒存在了,所以退出了;
2.4.2 守護執行緒總結
守護執行緒特性:
- 守護執行緒具備自動結束生命週期的特性
- 主要用於一些後臺任務
參考文獻:
[ 1 ] Java高併發程式設計詳解 汪文君著。–北京:機械工業出版社,2018年6月第1版