引言 現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的 ...
引言
現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的運行的太快了,把時間分成一段一段的,通過時間片輪轉分給多個任務交替執行。
把CPU的時間切片,分給不同的任務執行,而且執行的非常快,看上去就像在同時運行一樣。例如,網易雲執行50ms,瀏覽器執行50ms,word 執行50ms,人的感官根本感知不到。現在多數的電腦都是多核(多個 CPU )多線程,例如4核8線程(可以近似的看成8個 CPU ),也是把每個核心運行時間切片分給不同的任務交替執行。
進程與線程
進程(Process)是操作系統對一個正在運行的程式的一種抽象,我們可以進程簡單理解為操作系統中正在運行的一個軟體,即把一個任務稱之為一個進程,例如我們的網易雲音樂就是一個進程,瀏覽器又是另外一個進程。
線程(Thread)線程是一個比進程更小的執行單位,進程是線程的容器,一個進程至少有一個線程而且可以產生多個線程,每個線程都運行在進程的上下文中,並共用同樣的代碼和全局數據,多線程之間比多進程之間更容易共用數據,而且線程一般來說都比進程更加高效。
java語言內置了多線程支持:JVM 啟動時會創建一個主線程,該主線程負責執行 main 方法,一個 Java 程式實際上是一個JVM進程,JVM進程用一個主線程來執行main()方法內部,我們又可以啟動多個線程。此外,JVM還有負責垃圾回收的其他工作線程等。
創建線程
我們需要區分線程和線程體兩個概念,線程可以驅動任務,因此需要一個描述任務的方式,這個方式就是線程體,而我們創建線程體有多種方式,而創建線程只有一種:將任務(線程體)顯示的附著到線程上,調用 Thread 對象的 start()方法,執行線程的初始化操作,然後新線程調用 run() 方法啟動任務。
創建線程體可以使用下麵 3 種方式,然而這 3 種方式都是在創建線程體,直到調用 Thread 對象的 start() 方法時才請求 JVM 創建新的線程,具體什麼時候運行有線程調度器 Scheduler 決定。
- 繼承 Thread 類;
/**
* 1、定義Thread類的子類
*/
public class MyThread extends Thread {
//2、重寫Thread類的run方法
//run()方法體內的內容就是線程要執行的代碼
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
//3、創建線程對象
MyThread mt = new MyThread();
//4、啟動線程
mt.start();
/**
* 調用線程的start()方法來啟動線程,啟動線程的實質是請求JVM運行相應的線程,
* 這個線程具體什麼時候運行,由線程調度器(scheduler)決定
* 註意:
* 調用start()方法不代表線程能立馬運行
* 線程啟動後會運行run()方法
* 如果啟動了多個線程,start()調用的順序不一定就是線程啟動的順序
*/
}
- 實現 Runable 介面;
//1、實現Runnable介面
public class MyRunable implements Runnable{
//2、實現run方法
@Override
public void run() {
// ...
}
public static void main(String[] args) {
//3、將實現了Runnable介面的對象傳入Thread的構造方法中
Thread thread = new Thread(new MyRunable());
//4、啟動線程
thread.start();
}
}
- 實現 callable 介面
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableExample {
public static void main(String[] args) {
// 1、實現Callable介面的匿名內部類
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Callable task is running");
return 42;
}
};
// 2、將Callable包裝在RunnableFuture實現類中
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 3、將FutureTask實例傳遞給Thread類來執行
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在日常使用中,建議能用介面實現就不要用繼承 Thread 的方式來創建線程,原因如下:
- 避免單繼承的限制:Java是單繼承的語言,如果一個類繼承Thread類,就無法再繼承其他類。而實現Runnable介面則不會有這種限制,避免了單繼承的局限性。
- 更好的適配性:實現Runnable介面可以更好地支持類似線程池的機制,讓線程的執行和任務的分離更清晰。傳遞Runnable對象給線程池執行任務十分方便,而且可以重覆使用。
- 更好的面向對象設計:繼承Thread類是一種功能導向的設計,而實現Runnable介面更傾向於面向對象的設計,符合面向對象的編程思想。
線程的狀態
一個線程對象只能調用一次 start() 方法啟動新線程,併在新線程中執行 run() 方法,一旦 run() 方法 執行完畢,線程就終止死亡了。我們通過 Thread 類中的枚舉類 State 來看一下 Java 線程有哪些狀態:
public enum State {
/**
* 新建狀態
* 還沒有執行start()方法的線程狀態
*/
NEW,
/**
* 可運行狀態
* 在Java虛擬機中運行處於可運行狀態的線程,可能正在等待其他資源,例如處理器
*/
RUNNABLE,
/**
* 阻塞狀態
* 處於阻塞狀態的線程正在等待監視器鎖,以進入同步代碼塊或在調用wait()後重新進入
*/
BLOCKED,
/**
* 無限期等待狀態
* 線程因調用一下方法之一而處於無限期等待狀態:
* Object.wait with no timeout
* Thread.join with no timeout
* LockSupport.park
* 處於等待狀態的線程正在等待另一個線程執行特定操作
*/
WAITING,
/**
* 具有指定等待時間的等待線程的線程狀態
* 線程處於定時等待狀態的原因是調用了以下方法之一,並指定了正等待時間:
* Thread.sleep
* Object.wait with timeout
* Thread.join with timeout
* LockSupport.parkNanos
* LockSupport.parkUntil
*/
TIMED_WAITING,
/**
* 已終止線程的線程狀態.
* 線程已執行完畢.
*/
TERMINATED;
}
由源碼可知,Java 的線程狀態有 6 種:
- NEW:新創建的線程,還未執行;
- RUNNABLE:正在運行中線程或正在等待資源分配的準備運行的線程;
- BLOCKED:等待獲取監視器鎖的線程;
- WAITING:等待另外一個線程執行特定操作,沒有時間限制;
- TIMED_WAITING:等待某個特定線程在制定時間段內執行特定操作;
- TERMINATED:線程執行完畢
線程狀態的轉換可以參考下圖:
- NEW 狀態
創建線程後未啟動線程狀態為 NEW,在該線程調用 start() 方法以前會一直保持這種狀態。此時,JVM 會為該線程分配記憶體並初始化其成員變數的值,但是該線程並沒有表現出任何線程的動態特征,程式也不會執行線程的執行體,即 run() 方法的部分。
下麵的代碼,我們可以調用 Thread.getState() 方法來獲取線程的狀態,可以看出列印出來的狀態為 NEW。
public void ThreadTest() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ThreadTest");
}
});
System.out.println(t.getState()); // NEW
}
- RUNNABLE
當在Java的Thread對象上調用start()方法後,以下過程將會發生:
- 線程狀態變化:線程對象的狀態會從NEW(新建)狀態轉變為RUNNABLE(可運行)狀態,表明線程已經準備好運行,但尚未分配到CPU執行。
- 系統資源分配:線程調度器會為該線程分配系統資源,例如CPU時間。然而,並不保證立即執行,具體執行時機還取決於線程調度器的調度演算法和其他運行中的線程。
- 執行run()方法:當該線程被線程調度器選中並分配到CPU時間時,線程的run()方法會被調用,線程開始執行具體的任務邏輯。
處於RUNNABLE 狀態的線程要麼正在運行中,要麼已經準備好運行但正在等待系統分配 CPU 資源。
在Java虛擬機(JVM)中,JVM 自帶的線程調度器負責決定Java線程的執行順序。它會根據線程的優先順序和調度演算法來確定哪個線程可以獲得 CPU 時間。通常情況下,程式員可以通過設置線程的優先順序來影響線程調度器的決策,但實際線程的調度仍由 JVM 負責。
- BLOCKED
當線程嘗試訪問某個由其他線程鎖定的代碼塊時,該線程會因為需要等待獲取監視器鎖進入 BLOCKED 狀態,線程獲取鎖後就會結束此狀態。
- WAITING
線程正在等待另一個線程執行特定操作時處於 WAITING 等待狀態,例如當線程調用以下方法時會進入 WAITING 等待狀態:
調用方法 |
退出條件 |
Object.wait() |
Object.notify() / Object.notifyAll() |
Thread.join() |
被調用的線程(Thread)執行完畢 |
LockSupport.park() |
- |
上述方法中的 wait() 和 join() 沒有傳入超時時間 timeout 參數,線程只能等待其他線程顯示的喚醒或執行完畢,否則不會被分配 CPU 時間片。
- TIMED_WAITING
線程在這種狀態下屬於期限等待,無需其他線程顯示的喚醒當前線程,在一定時間內被系統自動喚醒。
阻塞和等待的區別在於:阻塞是被動的,等待是主動的。阻塞是在等待獲取鎖,而等待是在等待一定的條件發生。
調用方法 |
退出條件 |
Thread.sleep() |
時間結束 |
設置了 Timeout 參數的 Object.wait() 方法 |
時間結束 / Object.notify() / Object.notifyAll() |
設置了 Timeout 參數的 Thread.join() 方法 |
時間結束 / 被調用的線程執行完畢 |
LockSupport.parkNanos() 方法 |
- |
LockSupport.parkUntil() 方法 |
- |
- TERMINATED
線程執行完畢或者產生異常而結束會進入 TERMINATED 狀態,進入該狀態的線程已經死亡。
線程同步
併發問題產生的原因是:多個線程同時對一個共用資源進行非原子性操作,這裡麵包含了三個產生併發問題的三個條件:多個線程同時,共用資源,非原子性操作,解決線程安全問題的本質就是要破壞這三個條件,因此可以把多線程的並行執行,修改為單線程的串列執行,即同一時刻只讓一個線程執行,這種解決方式就叫做互斥鎖。
Java 提供了兩種鎖機制來控制多個線程對共用資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另一個是 JDK 實現的 ReentrantLock,從而達到保護共用資源的目的,當多條線程執行到被保護的區域時,都需要先去獲取到鎖,這時候只能有一條線程獲取到鎖,執行被保護區域的代碼,其他線程在保護區外部等待獲取鎖,直到當前線程執行完畢釋放資源後,其他線程才有執行的機會。
synchronized 和 ReentrantLock 可以保證可見性、原子性和有序性,另外一個 Java 的關鍵字 Volatile 也可以保證可見性,另外後者還可以禁止指令重排序。
Synchronized
在 Java 中每個對象都可以作為鎖,Synchronized 也是依賴 Java 的對象來實現鎖,一共有三種類型的鎖:
- 當前實例鎖:鎖定的是實例對象,即為 this 鎖;
- 類對象鎖:鎖定的是類對象,即為 Class 對象鎖;
- 對象實例鎖:鎖定的給定的對象實例,即位 Object 鎖;
在使用 Synchronize 時也有三種不同的方式:
- 修飾普通方法:使用 this 鎖,在執行該方法前必須先獲取當前實例對象的鎖資源;
- 修飾靜態方法:使用 class 鎖,在執行該方法前必須先獲取當前類對象的鎖資源;
- 修飾代碼塊:使用 Object 鎖,在執行該方法前必須先獲取給定對象的鎖資源;
public class A {
String lockObject = new String();
// 鎖定當前的實例,this鎖,每個實例擁有一個鎖
public synchronized void a() {};
// 修飾的是靜態方法,使用的 class 鎖,多個對象共用 class 鎖
public static synchronized void b() {}
public void c() {
// 修飾的是代碼塊,使用的 lockObject 對象的鎖,也是實例鎖
synchronized(lockObject) {
// do something
}
// 修飾代碼塊,使用的 B.class 類對象鎖
synchronized(B.class) {
}
}
}
public class B {
}
三種不同的使用方式有不同的應用場景,我們在使用的過程中一定要註意加鎖的對象是誰,否則可能會產生意想不到的結果。在加鎖時,儘量減少加鎖的區域,例如能夠在方法體中對代碼塊加鎖,就不要在方法上面加鎖,加鎖的區域越短越好。
ReentrantLock
ReentrantLock 是 Java.util.concurrent(J.U.C)包中的鎖,該鎖由 JDK 實現,而 synchronized 是由 JVM 實現的。
public class ReentrantLockDemo{
private Lock lock = new ReentrantLock();
private void func() {
lock.lock(); // 加鎖
try {
for (int i = 0; i < 10; i++) {
system.out.prrint(i)
}
} finally {
lock.unlock(); // 確保釋放鎖
}
}
}
public static void main(Stirng[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
上面的代碼演示了ReentrantLock 的使用方法,顯示的調用 lock()方法加鎖,在 finally 中顯示的釋放鎖。
鎖比較
不同點 |
synchronized |
reentrantLock |
實現方式 |
JVM |
JDK |
性能 |
新版本 Java 對 synchronized 進行了大量的優化,大致相同 |
|
等待可中斷 |
不可 |
可以 |
公平鎖 |
非公平 |
預設非公平,支持公平鎖 |
綁定多個條件 |
無 |
幫點多個 Condition 對象 |
在需要使用鎖時,除非需要使用 reentrantLock 的高級功能,否則優先使用 synchronized 關鍵字加鎖,這是因為 synchronized 是 JVM 實現的一種鎖機制,JVM 原生支持它,而 ReentrantLock 不是所有的 JDK 版本都支持,並且使用 syschronized 不用擔心沒有釋放鎖而導致死鎖問題,因為 JVM 會確保釋放鎖。
線程池
線程池可以管理一系列線程,當有任務需要處理時,直接從線程池裡面獲取線程來處理,當線程處理完任務時再放回到線程池中等待下一個任務,這樣可以減少每次創建線程的開銷,提升資源的利用率。線程池提供了一種限制和管理資源的方式,每個線程池還維護了一些基本的統計信息,例如 已完成任務的數量等。在《Java 併發編程的藝術》一書中提到使用線程有三點好處:
- 降低資源的消耗率:通過重覆利用已創建的線程,降低線程創建和銷毀造成的開銷;
- 提高響應速度:當任務到達時,任務不需要等待線程創建結束即可執行;
- 提高線程的可管理性:線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控;
創建線程池
可以使用內置的線程池,通過 Executor 框架的工具類 Executors 來創建預先定義好的線程池。
Executors
Executors 工具類提供的創建線程池的方法如下圖所示:
從上圖中可以看出,Executors 工具類可以創建多種類型的線程池,包括:
- FixedThreadPool:固定線程數量的線程池,在創建該線程池時,需要傳入一個線程池中線程個數的 int 參數,當有一個新的任務提交時,線程池中若有空閑線程,則立即執行。若沒有空閑線程,則新的任務會被暫存在一個任務隊列中,待有線程空閑時處理。
- SingleThreadPool:單線程線程池,在該線程池中只有一個線程,若超過一個線程提交到該線程池,任務會被保存到任務隊列中,等到該線程空閑時,按照先入先出的順序執行隊列中的任務。
- CachedThreadPool:可緩存線程的線程池,該線程池的線程數量不確定,在優先使用空閑線程的條件下,遇到新的任務提交時,會創建一個新的線程來處理任務,任務處理完畢後回到線程池等待復用。
- ScheduledExecutorPool:給定的延遲後運行的任務或定期執行任務的線程池。
自定義創建
如下圖,可以通過 ThreadPoolExecutor 構造函數來創建線程池(推薦)。
優先推薦使用 ThreadPoolExecutor 來創建線程池,在《阿裡巴巴 Java 開發手冊》中指出線程資源必須使用線程池來提供,不允許在應用中自行顯示創建線程,也強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 構造函數的方式來創建線程池。
使用內置的線程池有以下缺點:
- newFixedThreadPool 和 SingleThreadPool使用的是無界隊列 LinkedBlockingQueue,任務隊列最大成都為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM;
- CachedThreadPool:使用的是同步隊列 SyschronousQueue,允許創建的線程數量為 Integer.MAX_VALUE, 如果任務執行較慢,可能會創建大量的線程,從而導致 OOM。
- ScheduledExecutorPool:使用的無界的延遲阻塞隊列 DelayedWorkQueue,任務隊列最大長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM;
實際上內置的線程池也是調用 ThreadPoolExecutor 來創建的線程池:
// 無界隊列 LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 無界隊列 LinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 同步隊列 SynchronousQueue,沒有容量,最大線程數是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// DelayedWorkQueue(延遲阻塞隊列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
線程池參數
我們來看一下自定義創建線程池的參數有哪些?
public ThreadPoolExecutor(int corePoolSize, // 核心線程數
int maximumPoolSize, // 最大線程數
long keepAliveTime, // 當線程數大於核心線程數時,
// 多餘的空閑線程存活時間
TimeUnit unit, // 時間單位
BlockingQueue<Runnable> workQueue, // 任務隊列
ThreadFactory threadFactory, // 線程工廠,用於創建線程,一般預設
RejectedExecutionHandler handler) { // 拒絕策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
如上,ThreadPoolExecutor 中有三個很重要的參數
- corePoolSize:任務隊列未達到隊列容量時,最大可以同時運行的線程數量。
- maxinumPoolSize:任務隊列中存放的任務達到隊列容量時,當前可以同時運行的線程數量變為最大線程數。
- workQueue:新任務來時會先判斷當前運行的線程數量是否達到核心線程數,若達到核心線程數,新任務會被存放在隊列中。
ThreadPoolExecutor 其他常見參數:
- keepAliveTime:線程池中的線程數量超過 corePoolSize 時,如果這個時候沒有新的任務提交,核心線程以外的線程不會立即銷毀,會等到 keepAliveTime 的時間,然後才會銷毀超出部分的線程。
- unit:keepAliveTime 參數的時間單位。
- threadFactory:executor 創建新線程時用到。
- handler:拒絕策略
下麵這張圖可以看出,核心線程數量為 4,最大線程數量為 8。新任務提交到線程池時,首先判斷是否有線程或者線程數量是否小於核心線程數,若滿足則首先創建新的線程執行任務,當核心線程數到達 corePoolSize 時,將任務緩存到任務隊列中,當任務隊列存放的任務到達隊列容量時,再創建新的線程,直到達到 maxnumPoolSize 的線程數量,後續再根據拒絕策略返回。
拒絕策略
如果當前線程池同時運行的線程數量達到了最大線程數並且隊列也已經放滿了任務時,ThreadPoolExecutor 再接收到新的線程時,會執行一些預定義的拒絕策略,例如:
- ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException來拒絕新任務的處理。
- ThreadPoolExecutor.CallerRunsPolicy:調用執行自己的線程運行任務,也就是直接在調用execute方法的線程中運行(run)被拒絕的任務,如果執行程式已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程式的整體性能。如果您的應用程式可以承受此延遲並且你要求任何一個任務請求都要被執行的話,你可以選擇這個策略。
- ThreadPoolExecutor.DiscardPolicy:不處理新任務,直接丟棄掉。
- ThreadPoolExecutor.DiscardOldestPolicy:此策略將丟棄最早的未處理的任務請求。
ThreadPoolExecutor 預設執行的是 AbortPolicy,拋出 RejectedExecutionException 來拒絕新任務。如果不想丟棄任務,也可以使用 CallerRunsPolicy 將任務回退給調用者,使用調用者線程來執行任務。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 直接主線程執行,而不是線程池中的線程執行
r.run();
}
}
}
線程池任務處理流程
- 當新任務被提交到線程池時,首先判斷核心線程數量是否達到 corePoolSize,若未達到則創建新的線程,直到線程池中的線程數量到達 corePoolSize 的大小。
- 當核心線程數量達到 corePoolSize 的數量時,將新達到的任務緩存在阻塞隊列中,直到任務隊列容量用完,無法存放新的任務。
- 任務隊列無法存放新任務後,若線程池中的線程數量小於 maxnumPoolSize,則創建新的線程來執行任務,直到線程數量達到 maxnumPoolSize 的數量。
- 根據創建線程池時設置的拒絕策略來處理新提交的任務。
後記
本文從進程與線程、創建線程、線程狀態、線程同步和線程池等多個方面講述了線程基礎知識,希望大家對線程和線程池有了一個基礎的瞭解。後面我們將繼續深入多線程的其他知識,例如 Sychronized 的原理分析,JUC 工具類和 ThreadLocal 本地變數等知識,盡請關註。
因本人技術有限,如出現內容錯誤,請評論區糾正。碼字不易,點個關註再走吧~