多線程篇 (一)基礎篇
把學習當糖喫~會發現學習是一件很快樂的事情
目錄
多線程的基本概念
多線程: 嗯哼,說你呢是不是 一邊聽歌,一邊扣腳,一邊閱讀着文章,甚至還不忘記筆記~
單線程: 當然你也可以 先扣完腳,然後聽完歌,再去閱讀文章,完了最後記筆記~
1 Thread類和Runnable接口
如何使用多線程的呢?
- 繼承
Thread
類,並重寫run
方法; - 實現
Runnable
接口的run
方法;
1.1 繼承Thread類
先學會怎麼用,再學原理。首先我們來看看怎麼用Thread
和Runnable
來寫一個Java多線程程序。
首先是繼承Thread
類:
public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
注意要調用start()
方法後,該線程纔算啓動!
我們在程序裏面調用了start()方法後,虛擬機會先爲我們創建一個線程,然後等到這個線程第一次得到時間片時再調用run()方法。
注意不可多次調用start()方法。在第一次調用start()方法後,再次調用start()方法會拋出異常。
1.2 實現Runnable接口
接着我們來看一下Runnable
接口(JDK 1.8 +):
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到Runnable
是一個函數式接口,這意味着我們可以使用Java 8的函數式編程來簡化代碼。
示例代碼:
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
// Java 8 函數式編程,可以省略MyThread類
new Thread(() -> {
System.out.println("Java 8 匿名內部類");
}).start();
}
}
1.3 Thread類構造方法
Thread
類是一個Runnable
接口的實現類,我們來看看Thread
類的源碼。
查看Thread
類的構造方法,發現其實是簡單調用一個私有的init
方法來實現初始化。init
的方法簽名:
// Thread類源碼
// 片段1 - init方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
// 片段2 - 構造函數調用init方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 片段3 - 使用在init方法裏初始化AccessControlContext類型的私有屬性
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// 片段4 - 兩個對用於支持ThreadLocal的私有屬性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
init
方法的參數:
-
g:線程組,指定這個線程是在哪個線程組下;
-
target:指定要執行的任務;
-
name:線程的名字,多個線程的名字是可以重複的。如果不指定名字,見片段2;
-
acc:見片段3,用於初始化私有變量
inheritedAccessControlContext
。這個變量有點神奇。它是一個私有變量,但是在
Thread
類裏只有init
方法對它進行初始化,在exit
方法把它設爲null
。其它沒有任何地方使用它。一般我們是不會使用它的,那什麼時候會使用到這個變量呢?可以參考這個stackoverflow的問題:Restrict permissions to threads which execute third party software; -
inheritThreadLocals:可繼承的
ThreadLocal
,見片段4,Thread
類裏面有兩個私有屬性來支持ThreadLocal
,我們會在後面的章節介紹ThreadLocal
的概念。
實際情況下,我們大多是直接調用下面兩個構造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
1.4 Thread類的幾個常用方法
這裏介紹一下Thread類的幾個常用的方法:
- currentThread():靜態方法,返回對當前正在執行的線程對象的引用;
- start():開始執行線程的方法,java虛擬機會調用線程內的run()方法;
- yield():yield在英語裏有放棄的意思,同樣,這裏的yield()指的是當前線程願意讓出對當前處理器的佔用。這裏需要注意的是,就算當前線程調用了yield()方法,程序在調度的時候,也還有可能繼續運行這個線程的;
- sleep():靜態方法,使當前線程睡眠一段時間;
- join():使當前線程等待另一個線程執行完畢之後再繼續執行,內部調用的是Object類的wait方法實現的;
1.5 Thread類與Runnable接口的比較:
實現一個自定義的線程類,可以有繼承Thread
類或者實現Runnable
接口這兩種方式,它們之間有什麼優劣呢?
- 由於Java“單繼承,多實現”的特性,Runnable接口使用起來比Thread更靈活。
- Runnable接口出現更符合面向對象,將線程單獨進行對象的封裝。
- Runnable接口出現,降低了線程對象和線程任務的耦合性。
- 如果使用線程時不需要使用Thread類的諸多方法,顯然使用Runnable接口更爲輕量。
所以,我們通常優先使用“實現Runnable
接口”這種方式來自定義線程類。
2 Callable、Future與FutureTask
通常來說,我們使用Runnable
和Thread
來創建一個新的線程。但是它們有一個弊端,就是run
方法是沒有返回值的。而有時候我們希望開啓一個線程去執行一個任務,並且這個任務執行完成後有一個返回值。
JDK提供了Callable
接口與Future
接口爲我們解決這個問題,這也是所謂的“異步”模型。
2.1 Callable接口
Callable
與Runnable
類似,同樣是只有一個抽象方法的函數式接口。不同的是,Callable
提供的方法是有返回值的,而且支持泛型。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
那一般是怎麼使用Callable
的呢?Callable
一般是配合線程池工具ExecutorService
來使用的。我們會在後續章節解釋線程池的使用。這裏只介紹ExecutorService
可以使用submit
方法來讓一個Callable
接口執行。它會返回一個Future
,我們後續的程序可以通過這個Future
的get
方法得到結果。
這裏可以看一個簡單的使用demo:
// 自定義Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模擬計算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意調用get方法會阻塞當前線程,直到得到結果。
// 所以實際編碼中建議使用可以設置超時時間的重載get方法。
System.out.println(result.get());
}
}
輸出結果:
2
2.2 Future接口
Future
接口只有幾個比較簡單的方法:
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
cancel
方法是試圖取消一個線程的執行。
注意是試圖取消,並不一定能取消成功。因爲任務可能已完成、已取消、或者一些其它因素不能取消,存在取消失敗的可能。boolean
類型的返回值是“是否取消成功”的意思。參數paramBoolean
表示是否採用中斷的方式取消線程執行。
所以有時候,爲了讓任務有能夠取消的功能,就使用Callable
來代替Runnable
。如果爲了可取消性而使用 Future
但又不提供可用的結果,則可以聲明 Future<?>
形式類型、並返回 null
作爲底層任務的結果。
2.3 FutureTask類
上面介紹了Future
接口。這個接口有一個實現類叫FutureTask
。FutureTask
是實現的RunnableFuture
接口的,而RunnableFuture
接口同時繼承了Runnable
接口和Future
接口:
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
類有什麼用?爲什麼要有一個FutureTask
類?前面說到了Future
只是一個接口,而它裏面的cancel
,get
,isDone
等方法要自己實現起來都是非常複雜的。所以JDK提供了一個FutureTask
類來供我們使用。
示例代碼:
// 自定義Callable,與上面一樣
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模擬計算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
使用上與第一個Demo有一點小的區別。首先,調用submit
方法是沒有返回值的。這裏實際上是調用的submit(Runnable task)
方法,而上面的Demo,調用的是submit(Callable<T> task)
方法。
然後,這裏是使用FutureTask
直接取get
取值,而上面的Demo是通過submit
方法返回的Future
去取值。
在很多高併發的環境下,有可能Callable和FutureTask會創建多次。FutureTask能夠在高併發環境下確保任務只執行一次。這塊有興趣的同學可以參看FutureTask源碼。
注:
部分摘抄 深入淺出Java多線程, 這個系列目前一共20章~我已經看完了 ~側重點在解析原理,也貼心的補了簡單的案例以及圖文,
在實戰及案例一塊就需要自己去實踐了~ 得是真的很棒
原文簡介:
站在巨人的肩上,我們可以看得更遠。本書內容的主要來源有博客、書籍、論文,對於一些已經敘述得很清晰的知識點我們直接引用在本書中;對於一些沒有講解清楚的知識點,我們加以畫圖或者編寫Demo進行加工;而對於一些模棱兩可的知識點,本書在查閱了大量資料的情況下,給出最合理的解釋。
算是記錄一下自己的學習, 把好東西分享給更多人,素質三連😄