今天,我們開始Java高併發與多執行緒的第二篇,執行緒的實現方式。
通常來講,執行緒有三種基礎實現方式,一種是繼承Thread類,一種是實現Runnable介面,還有一種是實現Callable介面,當然,如果我們鋪開,擴充套件一下,會有很多種實現方式,但是歸根溯源,其實都是這幾種實現方式的衍生和變種。
我們依次來講。
【第一種 · 繼承Thread】
繼承Thread之後,要實現父類的run方法,然後在起執行緒的時候,呼叫其start方法。
1 public class DemoThreadDemoThread extends Thread {
2 public void run() {
3 System.out.println("我執行了一次!");
4 }
5
6 public static void main(String[] args) throws InterruptedException {
7 for (int i = 0; i < 3; i++) {
8 Thread thread = new DemoThreadDemoThread();
9 thread.start();
10 Thread.sleep(3000);
11 }
12 }
13 }
這裡,有些同學對start方法和run方法的作用理解有點問題,解釋一下:
start()方法被用來啟動新建立的執行緒,在start()內部呼叫了run()方法。 當你呼叫run()方法的時候,其實就和呼叫一個普通的方法沒有區別。 |
【第二種 · 實現Runnable介面】
第二種方法,是實現Runnable介面的run方法,在起執行緒的時候,如下,new一個Thread類,然後把類當做引數傳進去。
1 public class DemoThreadDemoRunnable implements Runnable { 2 public void run() { 3 System.out.println("我執行了一次!"); 4 } 5 6 public static void main(String[] args) throws InterruptedException { 7 for (int i = 0; i < 3; i++) { 8 Thread thread = new Thread(new DemoThreadDemoRunnable()); 9 thread.start(); 10 Thread.sleep(3000); 11 } 12 } 13 }
- 兩種基礎實現方式的選擇
這兩種方法看起來其實差不多,但是平時使用的時候我們如何做選擇呢?
一般而言,我們選擇Runnable會好一點;
首先,我們從程式碼的架構考慮。
實際上,Runnable 裡只有一個 run() 方法,它定義了需要執行的內容,在這種情況下,實現了 Runnable 與 Thread 類的解耦,Thread 類負責執行緒啟動和屬性設定等內容,權責分明。
第二點就是在某些情況下可以提高效能。
使用繼承 Thread 類方式,每次執行一次任務,都需要新建一個獨立的執行緒,執行完任務後執行緒走到生命週期的盡頭被銷燬,如果還想執行這個任務,就必須再新建一個繼承了 Thread 類的類,如果此時執行的內容比較少,比如只是在 run() 方法裡簡單列印一行文字,那麼它所帶來的開銷並不大,相比於整個執行緒從開始建立到執行完畢被銷燬,這一系列的操作比 run() 方法列印文字本身帶來的開銷要大得多。
如果我們使用實現 Runnable 介面的方式,就可以把任務直接傳入執行緒池,使用一些固定的執行緒來完成任務,不需要每次新建銷燬執行緒,大大降低了效能開銷。
第三點好處在於可以繼承其他父類。
Java 語言不支援雙繼承,如果我們的類一旦繼承了 Thread 類,那麼它後續就沒有辦法再繼承其他的類,這樣一來,如果未來這個類需要繼承其他類實現一些功能上的擴充,它就沒有辦法做到了,相當於限制了程式碼未來的可擴充性。
- 為什麼說Runnable和Thread其實是同一種建立執行緒的方法?
首先,我們可以去看看Thread的原始碼,我們可以看到:
其實Thread類也是實現了Runnable介面,以下是Thread的run方法:
其中的target指的就是你是實現了Runnable介面的類:
Thread被new出來之後,呼叫Thread的start方法,會呼叫其run方法,然後其實執行的就是Runnable介面的run方法(實現後的方法)。
所以,當你繼承了Thread之後,你實現的run方法其實還是Runnable介面的run方法,
因此,其實無返回值執行緒的實現方式只有一種,那就是實現Runnable介面的run方法,然後呼叫Thread的start方法,start方法內部會呼叫你寫的run方法,完成程式碼邏輯。
【第三種 · 實現Callable介面】
第 3 種執行緒建立方式是通過有返回值的 Callable 建立執行緒。
Callable介面實際上是屬於Executor框架中的功能類,之後的部分會詳細講述Executor框架。
Callable和Runnable可以認為是兄弟關係,Callable的call()方法類似於Runnable介面中run()方法,都定義任務要完成的工作,實現這兩個介面時要分別重寫這兩個方法;
主要的不同之處是call()方法是有返回值的,執行Callable任務可以拿到一個Future物件,表示非同步計算的結果。
我們通過Future物件可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。
程式碼示例如下:
1 public class DemoThreadDemoCallable implements Callable<Integer> { 2 3 private volatile static int count = 0; 4 5 public Integer call() { 6 System.out.println("我是callable,我執行了一次"); 7 return count++; 8 } 9 10 public static void main(String[] args) throws InterruptedException, ExecutionException { 11 List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>(); 12 13 for (int i = 0; i < 3; i++) { 14 FutureTask<Integer> futureTask = new FutureTask<Integer>(new DemoThreadDemoCallable()); 15 taskList.add(futureTask); 16 } 17 18 for (FutureTask<Integer> task : taskList) { 19 Thread.sleep(1000); 20 new Thread(task).start(); 21 } 22 23 // 一般這個時候是做一些別的事情 24 Thread.sleep(3000); 25 for (FutureTask<Integer> task : taskList) { 26 if (task.isDone()) { 27 System.out.printf("我是第%s次執行的!\n", task.get()); 28 } 29 } 30 } 31 }
執行結果如下:
其中,count 使用volatile 修飾了一下,關於volatile 關鍵字,之後會再次講到,這裡先不說。 |
當然了,我們一般情況下,都會使用執行緒池來進行Callable的呼叫,如下程式碼所示。
1 public class DemoThreadDemoCallablePool { 2 public static void main(String[] args) { 3 ExecutorService threadPool = Executors.newSingleThreadExecutor(); 4 Future<Integer> future = threadPool.submit(new DemoThreadDemoCallable()); 5 try { 6 System.out.println(future.get()); 7 } catch (Exception e) { 8 System.err.print(e.getMessage()); 9 } finally { 10 threadPool.shutdown(); 11 } 12 } 13 }
- Callable和Runnable的區別
最後,我們來看看Callable和Runnable的區別:
- Callable規定的方法是call(),而Runnable規定的方法是run()
- Callable的任務執行後可返回值,而Runnable的任務是不能返回值的(執行Callable任務可拿到一個Future物件, Future表示非同步計算的結果)
- call()方法可丟擲異常,而run()方法是不能丟擲異常的
【三種建立執行緒的基礎方式圖示】
通過圖示基本上可以看得很清楚,Callable是需要Future介面的實現類搭配使用的;
Future介面一共有5個方法:
-
- boolean cancel(boolean mayInterruptIfRunning)
用於取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。
引數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設定true,則表示可以取消正在執行過程中的任務。
如果任務已經完成,此方法返回false,即取消已經完成的任務會返回false;
如果任務正在執行,若mayInterruptIfRunning設定為true,則返回true,若mayInterruptIfRunning設定為false,則返回false;
如果任務還沒有執行,則無論mayInterruptIfRunning為true還是false,肯定返回true。
-
- boolean isCancelled()
如果在Callable任務正常完成前被取消,返回True
-
- boolean isDone()
若Callable任務完成,返回True
-
- V get() throws InterruptedException, ExecutionException
返回Callable裡call()方法的返回值,呼叫這個方法會導致程式阻塞,必須等到子執行緒結束後才會得到返回值
-
- V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null
事實上,FutureTask是Future介面唯一一個實現類。(只看了1.7和1.8,後面的版本沒有check)
【JVM執行緒模型】
在上一節的時候我們講了執行緒的生命週期和狀態,其實每一個執行緒,從建立到銷燬,都佔用了大量的CPU和記憶體,但是真正執行任務鎖佔用的資源可能並沒有很多,可能只能佔用到裡面很少的一部分。
在java裡面,使用執行緒執行非同步任務的時候,如果每次都建立一個新的執行緒,那麼系統資源會大量被佔用,伺服器會一直處於超高負荷的運算,最終很容易就會造成系統崩潰。
從jdk1.5開始,java將工作單元和執行機制分離,工作單元主要包括Runnable和Callable,執行機制就是Executor框架。
在HotSpot VM的執行緒模型中,Java執行緒會被一對一的對映成本地作業系統執行緒。(Java執行緒建立的時候,作業系統裡面會對應建立一個執行緒,Java執行緒被銷燬,作業系統中的執行緒也被銷燬)
在應用層,Java多執行緒程式會將整個應用分解成多個任務,然後執行的時候,會使用Executor將這些任務分配給固定的一些執行緒去分別執行,而底層作業系統核心會將這些執行緒對映到硬體處理器上,呼叫CPU進行執行。
類似下圖:
【Executor框架】
以上,是Executor框架結構圖解。
Executor框架包括3大部分:
-
- 任務
也就是工作單元,包括被執行任務需要實現的介面(Runnable/Callable)
-
- 任務的執行
把任務分派給多個執行緒的執行機制,包括Executor介面及繼承自Executor介面的ExecutorService介面。
-
- 非同步計算的結果
包括Future介面及實現了Future介面的FutureTask類。
-
- Executor
Executor框架的基礎,將任務的提交和執行分離開。
-
- TreadPoolExecutor
執行緒池的核心實現類,用來執行被提交的任務。
通常使用Executors建立,一共有三種型別的ThreadPoolExecutor:
-
-
- SingleThreadExecutor(單執行緒)
- FixedThreadPool(限制當前執行緒數量)
- CachedThreadPool(大小無界的執行緒池)
-
-
- ScheduledThreadPoolExecutor
實現類,可以在一個給定的延遲時間後,執行命令,或者定期執行。
通常也是由Executor建立,一共有兩種型別的ScheduledThreadPoolExecutor。
-
-
- ScheduledThreadPoolExecutor(包含多個執行緒,週期性執行)
- SingleThreadScheduledExecutor(只包含一個執行緒)
-
-
- Future
非同步計算的結果。
這裡在很多時候返回的是一個FutureTask的物件,但是並不是必須是FutureTask,只要是Future的實現類即可。
【TreadPoolExecutor】
之前已經說過,TreadPoolExecutor是執行緒池的核心實現類,所有執行緒池的底層實現都依靠它的建構函式:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
我們可以依次解釋一下:
- corePoolSize
核心執行緒數,也叫常駐執行緒數,執行緒池裡從建立之後會一直存在的執行緒數量。
- maximumPoolSize
最大執行緒數,執行緒池中最多存在的執行緒數量。
- keepAliveTime
空閒執行緒的存活時間,一個執行緒執行完任務之後,等待多久會被執行緒池回收。
- unit
keepAliveTime引數的單位
- workQueue
執行緒佇列(一般為阻塞佇列)
- threadFactory
執行緒工廠,用於建立執行緒
- handler
拒絕策略,由於達到執行緒邊界和佇列容量而阻止執行時使用的處理程式。
其實,執行緒池不能算作一種建立執行緒的方式,為什麼呢?
對於執行緒池而言,本質上是通過執行緒工廠建立執行緒的。 預設採用 DefaultThreadFactory ,它會給執行緒池建立的執行緒設定一些預設值,比如:執行緒的名字、是否是守護執行緒,以及執行緒的優先順序等。
|
關於執行緒池的詳細概念內容,可先參考我的另外一篇部落格:執行緒池略略觀,後面也會再說。
關於執行緒的主要建立方式,大概就是以上這些,下一篇,會講執行緒的基本屬性和主要方法。