深入理解程序、執行緒、執行緒池的區別和聯絡
我們在學習JAVA或者面試過程中,往往會碰到程序、 執行緒、執行緒池的之間的錯綜關係,下面我結合網上的資料和自己的理解,總結了三者的關係,從以下幾個方面說起:
1、程序、執行緒、執行緒池的概念
程序是一個動態的過程,是一個活動的實體。簡單來說,一個應用程式的執行就可以被看做是一個程序,而執行緒,是執行中的實際的任務執行者。可以說,程序中包含了多個可以同時執行的執行緒。
執行緒,程式執行流的最小執行單位,是程序中的實際運作單位。
執行緒池:Java中開闢出了一種管理執行緒的概念,這個概念叫做執行緒池,從概念以及應用場景中,我們可以看出,執行緒池的好處,就是可以方便的管理執行緒,也可以減少記憶體的消耗,那為什麼我們要使用執行緒池,主要解決如下幾個問題:
- 建立/銷燬執行緒伴隨著系統開銷,過於頻繁的建立/銷燬執行緒,會很大程度上影響處理效率
- 執行緒併發數量過多,搶佔系統資源,從而導致系統阻塞
- 能夠容易的管理執行緒,比如:執行緒延遲執行、執行策略等
2、執行緒的 生命週期
執行緒的生命週期,執行緒的生命週期可以利用以下的圖解來更好的理解:
首先使用new Thread()的方法新建一個執行緒,線上程建立完成之後,執行緒就進入了就緒(Runnable)狀態,此時創建出來的執行緒進入搶佔CPU資源的狀態,當執行緒搶到了CPU的執行權之後,執行緒就進入了執行狀態(Running),當該執行緒的任務執行完成之後或者是非常態的呼叫的stop()方法之後,執行緒就進入了死亡狀態。而我們在圖解中可以看出,執行緒還具有一個則色的過程,這是怎麼回事呢?當面對以下幾種情況的時候,容易造成執行緒阻塞,第一種,當執行緒主動呼叫了sleep()方法時,執行緒會進入則阻塞狀態,除此之外,當執行緒中主動呼叫了阻塞時的IO方法時,這個方法有一個返回引數,當引數返回之前,執行緒也會進入阻塞狀態,還有一種情況,當執行緒進入正在等待某個通知時,會進入阻塞狀態。那麼,為什麼會有阻塞狀態出現呢?我們都知道,CPU的資源是十分寶貴的,所以,當執行緒正在進行某種不確定時長的任務時,Java就會收回CPU的執行權,從而合理應用CPU的資源。我們根據圖可以看出,執行緒在阻塞過程結束之後,會重新進入就緒狀態,重新搶奪CPU資源。這時候,我們可能會產生一個疑問,如何跳出阻塞過程呢?又以上幾種可能造成執行緒阻塞的情況來看,都是存在一個時間限制的,當sleep()方法的睡眠時長過去後,執行緒就自動跳出了阻塞狀態,第二種則是在返回了一個引數之後,在獲取到了等待的通知時,就自動跳出了執行緒的阻塞過程。
3、單執行緒和多執行緒概念
單執行緒,顧名思義即是隻有一個執行緒在執行任務,這種情況在我們日常的工作學習中很少遇到,所以我們只是簡單做一下了解
多執行緒,建立多個執行緒同時執行任務,這種方式在我們的日常生活中比較常見。但是,在多執行緒的使用過程中,還有許多需要我們瞭解的概念。比如,在理解上並行和併發的區別,以及在實際應用的過程中多執行緒的安全問題,對此,我們需要進行詳細的瞭解。
並行和併發:在我們看來,都是可以同時執行多種任務,那麼,到底他們二者有什麼區別呢?
併發:從巨集觀方面來說,併發就是同時進行多種時間,實際上,這幾種時間,並不是同時進行的,而是交替進行的,而由於CPU的運算速度非常的快,會造成我們的一種錯覺,就是在同一時間內進行了多種事情
並行:則是真正意義上的同時進行多種事情。這種只可以在多核CPU的基礎上完成。
還有就是多執行緒的安全問題?為什麼會造成多執行緒的安全問題呢?我們可以想象一下,如果多個執行緒同時執行一個任務,意味著他們共享同一種資源,由於執行緒CPU的資源不一定可以被誰搶佔到,這是,第一條執行緒先搶佔到CPU資源,他剛剛進行了第一次操作,而此時第二條執行緒搶佔到了CPU的資源,共享資源還來不及發生變化,就同時有兩個執行緒使用了同一條資源,會造成資料不一致性,導致執行緒執行錯誤發生。
有造成問題的原因我們可以看出,這個問題主要的矛盾在於,CPU的使用權搶佔和資源的共享發生了衝突,解決時,我們只需要讓一條執行緒佔用了CPU的資源時,阻止第二條執行緒同時搶佔CPU的執行權,在程式碼中,我們只需要在方法中使用同步程式碼塊即可。在這裡,同步程式碼塊不多進行贅述,詳情檢視https://blog.csdn.net/gyshun/article/details/81626942去了解。
4、JAVA中執行緒池的實現
在Java中,執行緒池的概念是Executor這個介面,具體實現為ThreadPoolExecutor類,學習Java中的執行緒池,就可以直接學習它。對執行緒池的配置,就是對ThreadPoolExecutor建構函式的引數的配置,既然這些引數這麼重要,就來看看建構函式的各個引數吧
ThreadPoolExecutor提供了四個建構函式
//五個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
//六個引數的建構函式-1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
//六個引數的建構函式-2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
//七個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其它這四個建構函式,一共牽涉到7個引數型別,下面主要講解七個引數。
-
int corePoolSize => 該執行緒池中核心執行緒數最大值
核心執行緒:
執行緒池新建執行緒的時候,如果當前執行緒總數小於corePoolSize,則新建的是核心執行緒,如果超過corePoolSize,則新建的是非核心執行緒
核心執行緒預設情況下會一直存活線上程池中,即使這個核心執行緒啥也不幹(閒置狀態)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut這個屬性為true,那麼核心執行緒如果不幹活(閒置狀態)的話,超過一定時間(時長下面引數決定),就會被銷燬掉
很好理解吧,正常情況下你不幹活我也養你,因為我總有用到你的時候,但有時候特殊情況(比如我自己都養不起了),那你不幹活我就要把你幹掉了
-
int maximumPoolSize
該執行緒池中執行緒總數最大值
執行緒總數 = 核心執行緒數 + 非核心執行緒數。核心執行緒在上面解釋過了,這裡說下非核心執行緒:
不是核心執行緒的執行緒(別激動,把刀放下…),其實在上面解釋過了
-
long keepAliveTime
該執行緒池中非核心執行緒閒置超時時長
一個非核心執行緒,如果不幹活(閒置狀態)的時長超過這個引數所設定的時長,就會被銷燬掉
如果設定allowCoreThreadTimeOut = true,則會作用於核心執行緒
-
TimeUnit unit
keepAliveTime的單位,TimeUnit是一個列舉型別,其包括:
- NANOSECONDS : 1微毫秒 = 1微秒 / 1000
- MICROSECONDS : 1微秒 = 1毫秒 / 1000
- MILLISECONDS : 1毫秒 = 1秒 /1000
- SECONDS : 秒
- MINUTES : 分
- HOURS : 小時
- DAYS : 天
-
BlockingQueue workQueue
該執行緒池中的任務佇列:維護著等待執行的Runnable物件
當所有的核心執行緒都在幹活時,新新增的任務會被新增到這個佇列中等待處理,如果佇列滿了,則新建非核心執行緒執行任務
常用的workQueue型別:
-
SynchronousQueue:這個佇列接收到任務的時候,會直接提交給執行緒處理,而不保留它,如果所有執行緒都在工作怎麼辦?那就新建一個執行緒來處理這個任務!所以為了保證不出現<執行緒數達到了maximumPoolSize而不能新建執行緒>的錯誤,使用這個型別佇列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大
-
LinkedBlockingQueue:這個佇列接收到任務的時候,如果當前執行緒數小於核心執行緒數,則新建執行緒(核心執行緒)處理任務;如果當前執行緒數等於核心執行緒數,則進入佇列等待。由於這個佇列沒有最大值限制,即所有超過核心執行緒數的任務都將被新增到佇列中,這也就導致了maximumPoolSize的設定失效,因為匯流排程數永遠不會超過corePoolSize
-
ArrayBlockingQueue:可以限定佇列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建執行緒(核心執行緒)執行任務,如果達到了,則入隊等候,如果佇列已滿,則新建執行緒(非核心執行緒)執行任務,又如果匯流排程數到了maximumPoolSize,並且佇列也滿了,則發生錯誤
-
DelayQueue:佇列內元素必須實現Delayed介面,這就意味著你傳進去的任務必須先實現Delayed介面。這個佇列接收到任務時,首先先入隊,只有達到了指定的延時時間,才會執行任務
-
-
ThreadFactory threadFactory
建立執行緒的方式,這是一個介面,你new他的時候需要實現他的
Thread newThread(Runnable r)
方法,一般用不上。小夥伴應該知道AsyncTask是對執行緒池的封裝吧?那就直接放一個AsyncTask新建執行緒池的threadFactory引數原始碼吧:
new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread new Thread(Runnable r) { return new Thread(r,"AsyncTask #" + mCount.getAndIncrement()); } }
-
RejectedExecutionHandler handler
這玩意兒就是丟擲異常專用的,比如上面提到的兩個錯誤發生了,就會由這個handler丟擲異常,你不指定他也有個預設的拋異常能丟擲什麼花樣來?一般情況下根本用不上。
新建一個執行緒池的時候,一般只用5個引數的建構函式。
向ThreadPoolExecutor新增任務
那說了這麼多,你可能有疑惑,我知道new一個ThreadPoolExecutor,大概知道各個引數是幹嘛的,可是我new完了,怎麼向執行緒池提交一個要執行的任務啊?
通過ThreadPoolExecutor.execute(Runnable command)
方法即可向執行緒池內新增一個任務
ThreadPoolExecutor的策略
上面介紹引數的時候其實已經說到了ThreadPoolExecutor執行的策略,這裡給總結一下,當一個任務被新增進執行緒池時:
- 執行緒數量未達到corePoolSize,則新建一個執行緒(核心執行緒)執行任務
- 執行緒數量達到了corePools,則將任務移入佇列等待
- 佇列已滿,新建執行緒(非核心執行緒)執行任務
- 佇列已滿,匯流排程數又達到了maximumPoolSize,就會由上面那位星期天(RejectedExecutionHandler)丟擲異常
5、常見四種執行緒池
如果你不想自己寫一個執行緒池,那麼你可以從下面看看有沒有符合你要求的(一般都夠用了),如果有,那麼很好你直接用就行了,如果沒有,那你就老老實實自己去寫一個吧
Java通過Executors提供了四種執行緒池,這四種執行緒池都是直接或間接配置ThreadPoolExecutor的引數實現的,下面我都會貼出這四種執行緒池建構函式的原始碼,各位大佬們一看便知!
來,走起:
CachedThreadPool()
可快取執行緒池:
- 執行緒數無限制(沒有核心執行緒,全部是非核心執行緒)
- 有空閒執行緒則複用空閒執行緒,若無空閒執行緒則新建執行緒
- 一定程式減少頻繁建立/銷燬執行緒,減少系統開銷
適用場景:適用於耗時少,任務量大的情況
建立方法:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
原始碼:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
FixedThreadPool()
定長執行緒池:
- 有核心執行緒,核心執行緒數就是執行緒的最大數量(沒有非核心執行緒)
- 可控制執行緒最大併發數(同時執行的執行緒數)
- 超出的執行緒會在佇列中等待
建立方法:
//nThreads => 最大執行緒數即maximumPoolSize
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
//threadFactory => 建立執行緒的方法!
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);
原始碼:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2個引數的構造方法原始碼,不用我貼你也知道他把星期六放在了哪個位置!所以我就不貼了,省下篇幅給我扯皮
ScheduledThreadPool()
定長執行緒池:
- 支援定時及週期性任務執行。
- 有核心執行緒,也有非核心執行緒
- 非核心執行緒數量為無限大
適用場景:適用於執行週期性任務
建立方法:
//nThreads => 最大執行緒數即maximumPoolSize
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
原始碼:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
SingleThreadExecutor()
單執行緒化的執行緒池:
- 有且僅有一個工作執行緒執行任務
- 所有任務按照指定順序執行,即遵循佇列的入隊出隊規則
適用場景:適用於有順序的任務應用場景
建立方法:
ExecutorService singleThreadPool = Executors.newSingleThreadPool();
原始碼:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
還有一個Executors.newSingleThreadScheduledExecutor()
結合了3和4,就不介紹了,基本不用。