前言
前幾章都在講一些鎖的使用和原理,主要是爲了保證多線程情況下變量的原子性,但這並不是說多線程不好
,合理利用還是有好處的。至於什麼好處,看下面內容就懂了,先打個比方吧(誰叫比方,上來捱打
):假如你體育考試,要跑1000米,你現在有兩個選擇:
一個人跑完1000米。 找三個人陪你一起跑,每個人跑250米就好
兩種方案你選哪個?
今天寫一下面試必問的內容:多線程與線程池。主要從以下幾方面來說:
什麼是線程(什麼是多線程) 線程狀態 多線程的優點和弊端 線程池的好處 線程池的新建 線程池狀態 線程池執行任務 線程池異常處理 爲什麼submit()方法提交任務產生異常會被"吞掉"
6月6日,好吉利的數字,祝大家六六大順,話不多說,開始搞事!
1、什麼是線程(什麼是多線程)
在這裏先說一下進程,什麼是進程:昨晚我打開的久違的wegame,登錄了英雄聯盟客戶端,而這個客戶端,就是一個進程,客戶端進程名是Client.exe,不信你們試試!
線程是操作系統調度的最小單元,每個線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。而多個線程組成了一個進程。一個程序,比如愛奇藝客戶端、騰訊客戶端、wegam等,至少有一個進程,而一個進程至少有一個線程。
那麼問題來了,通過一個main方法啓動一個Java程序
,這算是進程還是線程呢?代碼如下:
public class ThreadTest {
public static void main(String[] args) {
}
}
答案是:進程
,改進一下上述代碼,執行如下:
public class ThreadTest {
public static void main(String[] args) {
// 獲取Java線程管理的MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// dumpAllThreads(boolean lockedMonitors, boolean lockedSynchronizers)
// 不需要獲取同步的synchronizer信息,僅獲取線程和線程堆棧晉西
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, false);
for (ThreadInfo threadInfo : threadInfos){
System.out.println(
"[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName()
);
}
}
}
main:main線程,程序入口 Reference Handler:清除Reference的線程(對象的引用存在虛擬機棧中,GC的時候需要用到) Finalizer:調用對象finalizer方法的線程(GC的時候需要用到) Signal Dispatcher:分發處理髮送給JVM信號的線程 Monitor Ctrl-Break:同步的monitor線程
看上面例子,能清楚的看到,一個Java程序的運行不僅是main()方法的運行,而是main線程和多個其他的線程同時運行,這就是多線程。
多線程:多線程就是指一個進程中同時有多個執行路徑(線程)正在執行
。
2、線程狀態
Java線程在運行的生命週期中可能處於下表所示的6種不同狀態,在給定的一個時刻,線程只能處於其中一個狀態。
狀態名稱 | 說明 |
---|---|
NEW | 初始狀態,線程被構建,但是還沒有調用start()方法 |
RUNNABLE | 運行狀態,Java線程將操作系統中的就緒和運行兩種狀態籠統的成爲“運行中” |
BLOCKED | 阻塞狀態,表示線程阻塞於鎖(這裏有個容易混淆的問題,下面會講) |
WAITING | 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷,想下前幾篇文章,進入同步隊列獲取鎖的過程,想想有什麼問題) |
TIME_WAITING | 超時等待狀態,該狀態不同於WAITING,它是可以在指定的時間自行返回的 |
TERMINATED | 終止狀態,表示當前線程已經執行完畢 |
注意,在上一篇講ReentrantLock(重入鎖)的時候,當已經有線程獲取了,其餘線程會進入同步隊列,嘗試自旋幾次之後調用LockSupport.park()方法,這個方法是用來阻塞當前線程,那麼在調用了這個方法之後,在同步隊列中線程的狀態是什麼呢?BLOCKED還是WAITING?答案是線程處於WAITING(等待狀態)
,阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊時的狀態,但是阻塞在Lock接口的線程狀態是等待狀態。
線程在自身的生命週期中狀態變遷圖如下所示:
3、多線程的優點和弊端
3.1 優點
多線程技術使程序的響應速度更快(
比如打開一個網頁,調用一個接口,這個接口創建了很多線程去數據庫讀取數據異步去返回數據,這樣用戶立馬就打開了網頁,裏面的內容的展現可能不是一起展示,但是總比空白界面停留十幾秒再全部展示出來好吧
);當前沒有進行處理的任務時可以將處理器時間讓給其它任務;
佔用大量處理時間的任務可以定期將處理器時間讓給其它任務;
可以隨時停止任務(
中斷某個線程
);可以分別設置各個任務的優先級以優化性能(
在線程構建的時候通過setPriority(int)方法來修改優先級,範圍爲1-10,默認優先級爲5,優先級高的線程分配時間片的數量要多於優先級低的線程
)。
一個線程在一個時刻只能運行在一個處理器核心上,使用多線程技術,將計算邏輯分配到多個處理器核心上,就會顯著減少程序的處理時間,變得更有效率:
3.2 弊端
等候使用共享資源時造成程序的運行速度變慢(
比如庫存,如果多個線程同時去扣減,就有可能變成負數,這樣是不被允許的,所以就需要之前幾篇文章講的鎖來控制,所以前一個線程獲取了資源,後一個線程就會被阻塞,造成程序的運行速度變慢
)。對線程進行管理要求額外的CPU開銷,線程的使用會給系統帶來上下文切換的額外負擔(
多線程是通過分配CPU時間片來實現的,時間片非常短,所以CPU會不停的切換線程執行,當一個線程執行一個時間片後會切到下一個任務,但是在切換之前會保存上一個任務的狀態,下次在切換回這個任務的時候,可以再加載這個任務的狀態,這就是上下文切換
)。線程的死鎖。即對共享資源加鎖實現同步的過程中可能會死鎖。
對公有變量的同時讀或寫,可能對造成髒讀等(
這就是鎖該做的事情
)。線程創建和銷燬會造成消耗(
這就是線程池該做的事情了
)。
4、線程池的好處
降低資源消耗( 可以通過重複利用已創建的線程來降低線程創建和銷燬造成的消耗
)。提高相應速度( 當任務到達時,任務可以不需要等到線程創建就能立即執行
)。提高線程的可管理性( 線程是稀缺資源,如果無限制創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控
)。
5、線程池的新建
public class ThreadTest {
/**
* 基於數組的有界阻塞隊列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 創建一個線程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new ThreadPoolExecutor.AbortPolicy());
// 上面是創建線程池的代碼,下面只是用來測試拒絕策略的
for (int i = 0; i < 30; i++){
threadPoolExecutor.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
});
}
}
}
corePoolSize:線程池核心線程數大小。
當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時不再創建。maximumPoolSize:線程池最大線程數量。
線程池允許創建的最大線程數。如果阻塞隊列滿了,並且已創建的線程數小於最大線程數,則線程池會在創建新的線程執行任務(如果使用了無界隊列,那麼這個參數就沒什麼用了
)。keepAliveTime:線程池中非核心線程空閒的存活時間大小。
unit:線程空閒存活時間單位。
workQueue:存放任務的阻塞隊列。
用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個隊列:
1、ArrayBlockingQueue
:是一個基於數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序。
2、LinkedBlockingQueue
:一個基於鏈表結構的阻塞隊列,此隊列按FIFO(先進先出)排序元素,吞吐量通常要高於ArrayBlockingQueue。
3、SynchronousQueue
:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作。否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue。
4、PriorityBlockingQueue
:一個具有優先級的無限阻塞隊列。threadFactory:用於設置創建線程的工廠,可以給創建的線程設置有意義的名字,可方便排查問題,也可以設置線程執行出現異常的處理策略(下面文章會講)。
如上圖,每個線程的name都以wx開頭。handler:線城池的飽和策略事件,主要有四種類型。
1、AbortPolicy
:直接拋出異常,如下圖: 2、CallerRunsPolicy
:用調用者所在線程來運行任務。
3、DiscardOldestPolicy
:丟棄隊列裏最近的一個任務,並執行當前任務。
4、DiscardPolicy
:不處理,丟棄掉。除了上面這四種,還有一種自定義策略,實現RejectedExecutionHandler接口即可:
public class Handler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印線程信息
System.out.println(r.toString());
}
}
6、線程池狀態
線程池的狀態主要有以下幾種:
狀態名稱 | 說明 |
---|---|
RUNNING | 初始狀態,能夠接收新任務,以及對已添加的任務進行處理 |
SHUTDOWN | 線程池處在SHUTDOWN狀態時,不接收新任務,但能處理已添加的任務,處理完成之後纔會退出。 |
STOP | 線程池處在STOP狀態時,不接收新任務,不處理已添加的任務,並且會中斷正在處理的任務。 |
TIDYING | 當所有的任務已終止,線程池會變爲TIDYING狀態。當線程池變爲TIDYING狀態時,會執行鉤子函數terminated()。terminated()在ThreadPoolExecutor類中是空的,若用戶想在線程池變爲TIDYING時,進行相應的處理,可以通過重載terminated()函數來實現。 |
TERMINATED | 線程池執行完鉤子函數terminated()之後,就變成TERMINATED狀態。 |
這裏有幾個要注意的點:
當調用了shutdown()方法之後,就會從RUNNING轉變爲SHUTDOWN狀態,此時不能再向線程池添加新任務,否則將會拋出RejectedExecutionException異常。 當調用了shutdownNow()方法之後,就會從RUNNING轉變爲STOP狀態,並試圖停止所有正在執行的線程,不再處理還在池隊列中等待的任務,當然,它會返回那些未執行的任務。 當線程池在SHUTDOWN狀態下,阻塞隊列爲空並且線程池中執行的任務也爲空時,就會由SHUTDOWN轉變爲TIDYING狀態。當線程池在STOP狀態下,線程池中執行的任務爲空時,就會由STOP轉變爲TIDYING狀態。
7、線程池執行任務
上面我們創建了一個線程池,並且通過execute()方法提交了任務,然後可以看出上面拋出了異常(拒絕策略), 爲什麼會這樣呢,下面可以看下線程池的處理流程圖就明白了:
線程池處理任務流程:
1: 通過判斷核心線程池裏的線程是否都在執行任務,如果不是,則創建一個線程去執行,如果核心線程都在執行任務。那麼就判斷阻塞隊列。 2: 判斷阻塞隊列是否已滿,如果沒滿,就將任務加到隊列中,如果滿了,就判斷創建的線程是否達到了最大數量( 所以這裏有個問題,如果你隊列是無界的,那麼可以一直往裏面添加任務,這就有可能引起內存溢出,這也是阿里官方手冊爲什麼建議用ThreadPoolExecutor去創建線程池了
)。3: ,判斷創建的線程是否達到了最大數量,如果沒有達到,就創建一個線程去執行任務,如果有達到,就執行拒絕策略( 默認的拒絕策略是拋出異常,就上面例子拋出的那個異常
)。
接下來看下execute()方法的源代碼:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 獲取當前有效的線程數和線程池的狀態
int c = ctl.get();
// 判斷正在運行線程數是否小於核心線程池,是則新創建一個線程執行任務,否則將任務放到任務隊列中
if (workerCountOf(c) < corePoolSize) {
// 在addWorker中創建工作線程執行任務
if (addWorker(command, true))
return;
c = ctl.get();
}
// 線程池是否處於運行狀態,且是否任務插入任務隊列成功
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 線程池是否處於運行狀態,如果不是則使剛剛的任務出隊
if (! isRunning(recheck) && remove(command))
// 執行拒絕策略
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 插入隊列不成功,且當前線程數數量小於最大線程池數量,此時則創建新線程執行任務,創建失敗的話就執行拒絕策略
else if (!addWorker(command, false))
reject(command);
}
可以看出,execute()方法是沒有返回值的
,所以你提交任務之後,是無法判斷任務是否被多線程執行成功,所以多線程還有一種提交方式,submit()方法,通過submit()方法提交任務,線程池會返回一個future類型的對象,通過這個future對象可以判斷任務是否提交成功,,並且可以通過future.get()來獲取返回值,get()方法會阻塞當前線程直到任務完成。實例如下:
public class ThreadTest {
/**
* 基於數組的有界阻塞隊列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 創建一個線程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
Future<?> submit = null;
// 上面是創建線程池的代碼,下面只是用來測試拒絕策略的
for (int i = 0; i < 10; i++){
int num = i;
submit = threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
});
try {
submit.get();
}catch (Exception e){
System.out.println("線程執行出現異常");
}
}
}
}
線程池異常處理
先來看一段代碼:
public class ThreadTest {
/**
* 基於數組的有界阻塞隊列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 創建一個線程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
// 上面是創建線程池的代碼,下面只是用來測試拒絕策略的
for (int i = 0; i < 10; i++){
int num = i;
threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
if (num == 4){
throw new RuntimeException();
}else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
});
}
}
}
上述代碼可以看到,當i=4的時候,會拋出一個異常,然後看下結果:
本該打印10行結果的,現在只打印了9行,執行報錯但是沒有拋出異常,這樣我們無法感知任務出現了異常,也就無法做相應處理。
但你把上面代碼的提交方式改爲execute()
,再次運行,你會發現有異常拋出的:
這是爲啥子呢,怎麼解決呢,先來說怎麼解決,再說爲啥submit()方法提交任務會將其中可能發生的異常喫
掉。解決方法如下:
添加try/catch捕獲異常
public class ThreadTest {
/**
* 基於數組的有界阻塞隊列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 創建一個線程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
// 上面是創建線程池的代碼,下面只是用來測試拒絕策略的
for (int i = 0; i < 10; i++){
int num = i;
threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
try {
if (num == 4){
throw new RuntimeException();
}else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}catch (Exception e){
System.out.println("線程:" + Thread.currentThread().getName() + "執行任務出現了異常");
}
}
});
}
}
}
查看結果:
利用submit()方法返回的future對象的get()方法來查看程序執行是否有異常產生:
public class ThreadTest {
/**
* 基於數組的有界阻塞隊列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 創建一個線程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder().setNameFormat("wx-%d").build(),
// handler
new Handler());
// 上面是創建線程池的代碼,下面只是用來測試拒絕策略的
for (int i = 0; i < 10; i++){
int num = i;
Future<?> submit = threadPoolExecutor.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
if (num == 4) {
throw new RuntimeException();
} else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
});
try {
submit.get();
}catch (Exception e){
System.out.println("線程:" + Thread.currentThread().getName() + "執行任務出現了異常");
}
}
}
}
查看結果:
你會發現,它不像上面那個try/catch具體到線程池內那個線程出現了問題,而是說你的主線程執行任務出現了異常
。
還有一種解決方案,這種異常解決方案是execute()方法提交的任務執行出現異常的處理方式,submit()方法提交的不適用。
在定義ThreadFactory的時候,調用setUncaughtExceptionHandler()方法來自定義異常處理方式:
public class ThreadTest {
private static final Logger logger = LoggerFactory.getLogger(ThreadTest.class);
/**
* 基於數組的有界阻塞隊列
*/
private static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
/**
* 創建一個線程池
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
10,
// keepAliveTime
10L,
// unit
TimeUnit.SECONDS,
// workQueue
arrayBlockingQueue,
// threadFactory
new ThreadFactoryBuilder()
// 設置線程名稱
.setNameFormat("wx-%d")
// 添加自定義異常處理方式:打印error日誌
.setUncaughtExceptionHandler((thread, throwable)-> logger.error("ThreadPoolExecutor {} produce exception", thread,throwable))
.build(),
// handler
new Handler());
// 上面是創建線程池的代碼,下面只是用來測試拒絕策略的
for (int i = 0; i < 10; i++){
int num = i;
threadPoolExecutor.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
if (num == 4) {
throw new RuntimeException();
} else {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
}
});
}
}
}
查看結果:
爲什麼submit()方法提交任務產生異常會被"吞掉"
說到這個問題,我們得先來看下submit()方法的源碼:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 任務被包裝成RunnableFuture對象,準備添加到工作隊列中
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
newTaskFor()方法代碼如下:
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
FutureTask類代碼如下:
public class FutureTask<V> implements RunnableFuture<V> {
......
}
RunnableFuture接口提供了一個run()方法:
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
看下FutureTask類的run()方法做了什麼:
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 捕獲異常
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
再看下setException()方法:
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 將異常放入outcome對象中
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
至此,我們可以看到,submit()方法其實是將任務包裝成RunnableFuture對象,其實最終是一個FutureTask實例,FutureTask實現了Future和Runnable接口。重寫了run(),而在run()方法裏面,該任務拋出的異常將被捕獲,通過setException()方法將異常放在outcome中
,這就是爲什麼沒有拋出異常的原因。
那麼問題來了,爲什麼調用submit()提交任務之後返回的FutureTask對象的get()方法就會看到異常呢,看get()方法源碼:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
report()方法代碼:
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
因爲get()方法會將存放異常的outcome對象返回出去
,這就是爲什麼調用submit()提交任務之後返回的FutureTask對象的get()方法就會看到異常的原因!
結尾
如果你覺得我的文章對你有幫助話,歡迎關注我的微信公衆號:"一個快樂又痛苦的程序員"(無廣告,單純分享原創文章、已pj的實用工具、各種Java學習資源,期待與你共同進步)