前言
人人皆知,多線程編程在充分利用計算資源、提高軟件服務質量方面扮演着非常重要的角色,然而,多線程編程並非一個簡單地使用多個線程進行編程的數量問題,其自身也有諸多問題,好比俗話說“一個和尚打水喝,兩個和尚挑水喝,三個和尚沒水喝”,簡單的使用多個線程進行過編程可能導致更加糟糕的計算效率。
所以讓我們來系統的探討一下Java多線程的奧祕吧。
一、無處不在的線程
進程(process)代表運行中的程序。一個運行的Java程序就是一個進程。從操作系統的角度來看,線程(Thread)是進程中可獨立執行的子任務。一個進程可以包含多個線程,同個進程中的線程共享該進程所申請到的資源,如內存空間和文件句柄等。從JVM的角度來看,線程是進程中的一個組件(Component),它可以看作執行Java代碼的最小單位。Java程序中的任何一段代碼總是執行在某個確定的線程中。JVM啓動時會創建一個main線程,負責執行Java程序的入口方法(main方法)。
下例1.1展示Java程序中代碼由某個確定的線程運行:
public class JavaThreadAnywhere {
public static void main(String[] args) {
System.out.println("The main method was executed by thread:" + Thread.currentThread().getName());
Helper helper = new Helper("Java Thread Anywhere");
helper.run();
}
static class Helper implements Runnable{
private final String message;
public Helper(String message) {
this.message = message;
}
private void doSomething(String message) {
System.out.println("The doSomething method was executed by thread:" + Thread.currentThread().getName());
System.out.println("Do something with " + message);
}
public void run() {
doSomething(message);
}
}
}
1.1運行結果:
在多線程編程中,弄清楚一段代碼具體是由哪個線程去負責執行是很重要的,關係到性能問題、線程安全問題等。
Java的線程可以分爲守護線程和用戶線程兩種,具體區別我們後面再細談。
一般來說,守護線程用於執行一些中重要性不高的任務,例如監視其他線程的運行狀況。
二、線程的創建與運行
在Java中, 一個線程就是一個java.lang.Thread類的實例。創建一個Thread實例(線程)與創建其他類的實例有所不同:JVM會爲一個Thread實例分配兩個調用棧(Call Stack)所需的空間。這兩個調用棧一個用於追蹤Java代碼間的調用關係,另一個用於追蹤Java代碼對本地代碼的調用關係。
一個Thread實例通常對應兩個線程。一個是JVM中的線程,而另一個是與JVM中的線程相對應的依賴於JVM宿主機操作系統的本地線程。啓動線程只需要調用start方法。線程啓動後,當相應的線程被JVM的線程調度器調度到運行,相應Thread實例的run方法會被JVM所調用。
下例1.2所示Java線程的創建與運行:
import java.lang.Thread;
public class JavaThreadAnywhere {
public static void main(String[] args) {
System.out.println("The main method was executed by thread:" + Thread.currentThread().getName());
Helper helper = new Helper("Java Thread Anywhere");
//創建一個線程
Thread thread = new Thread(helper);
//設置線程名
thread.setName("workThread");
//啓動線程
thread.start();
}
static class Helper implements Runnable{
private final String message;
public Helper(String message) {
this.message = message;
}
private void doSomething(String message) {
System.out.println("The doSomething method was executed by thread:" + Thread.currentThread().getName());
System.out.println("Do something with " + message);
}
public void run() {
doSomething(message);
}
}
}
1.2運行結果:
與1.1結果相比,同樣的Helper的同方法doSomething此時由線程workThread而非main線程執行。是因爲我們使用了workThread線程對代碼進行調用。
其中,對線程對象的start方法調用的操作是運行在main方法中的,而main方法是又main線程負責執行的,因此,我們所創建的線程thread就可以看成是main線程的一個子線程,而main線程則爲父線程。
Java中,子線程是否是一個守護線程取決於其父線程:默認情況下,父線程爲守護線程則子線程也是守護線程,反之亦然。當然,父線程在創建子線程後,啓動子線程之前可以調用Threa實例的setDaemon方法來修改線程的屬性。
Thread類自身是一個實現java.lang.Runnable接口的對象,我們可以定義一個Thread類的子類來創建線程,自定義的線程要覆蓋其父類的run方法。
如下例1.3所示:
public class ThreadCreationViaSubclass{
public static void main(String[] args) {
Thread thread = new CustomThread();
thread.run();
}
static class CustomThread extends Thread{
public void run() {
System.out.println("Running...");
}
}
}
三、線程的狀態與上下文切換
Java語言中,一個線程從創建、啓動到其運行結束的整個生命週期可能經歷若干個狀態,如圖:
Java線程的狀態可以通過調用相應Thread實例的getState方法來獲取,返回一個枚舉類型值(Enum),具體狀態在這裏不細談。
線程的狀態切換時,可能意味着上下文切換。上下文切換類似於我們接聽語音電話的場景。但我們在通話並討論某件事情時,突然有另外一個來電,這時我們會跟對方說:“我先接個電話,待會說”,並記下與之討論的進度。當處理完畢後再與第一個來電者進行通話,在此之前,若我們沒有特意的記下當前的討論進度,那再次開始討論時可能得詢問對方“我們說到哪裏了”。
多線程環境中,當一個線程的狀態由RUNNABLE轉換爲非RUNNABLE(BLOCKED、WAITING、TIMED_WAITING)時,相應線程的上下文信息(所謂的Context,包括CPU的寄存器和程序計數器在某一個時間節點的內容等)需要被保存,以便之後繼續進行。這個對線程的上下文信息進行保存和回覆的過程就被稱爲上下文切換。同時,上下文切換會帶來額外的開銷。
四、認識synchronized和volatile
首先我們要了解三個知識點:
- 原子性操作
指相應的操作是單一不可分割的操作。例如,對int型變量x進行x- - 的操作就不是原子操作:1.讀取x當前值 2.進行x- -數值運算 3.將2步驟的運算值賦值給變量x。 - 內存可見性
CPU在執行代碼時,爲了減少變量訪問的時間消耗可能將代碼中訪問的變量的值緩存到該CPU的緩存區中。因此相應的代碼再次訪問某個變量時,相應的值可能是從CPU緩存區讀取的。由於每個CPU都有自己的緩存區,而且每個CPU緩衝區的內容對其他CPU是不可見的。導致其他CPU上運行的其他線程可能無法“看到”該線程對某個變量的更改。 - 重排序
編譯器和CPU爲了提高指令的執行效率,可能會進行指令重排序,使得代碼的實際執行方式不是按照我們所認爲的方式進行的。
下面瞭解兩個關鍵字:
- synchronized
synchronized關鍵字可以幫助我們實現操作的原子性,避免線程間的干擾情況。通過該關鍵字所包括的臨界區的排他性保證在任何一個時刻只有一個線程能夠執行臨界區的代碼;
synchronized關鍵字還可以保證一個線程執行臨界區中的代碼時所修改的變量值對於稍後執行該臨界區中的代碼的線程來說是安全的。 - volatile
volatile關鍵字也可以保證內存可見性。其機制是:當一個線程修改了一個volatile修飾的變量的值時,該值會被寫入主內存而不僅僅是當前線程所在的CPU緩存區,而其他CPU緩存區中儲存的該變量的值因此失效。
volatile關鍵字還可以禁止指令重排序,對多線程代碼的正確性起到很大作用。
與synchronized關鍵字相比,volatile關鍵字僅能保證內存可見性,而前者既能保證操作的原子性,又能保證內存可見性。然而,前者會導致上下文切換,後者不會。
五、線程的優勢和風險
多線程具有以下優勢:
- 提高系統吞吐率
- 提高響應性
- 充分利用多核CPU資源
- 最小化對系統資源的使用
- 簡化程序的結構
多線程也有自身的問題和風險:
- 線程安全問題
- 線程生命特徵
- 上下文切換
- 可靠性