《面試補習》- 多線程知識梳理
一、基本概念
1.1、進程
進程是系統資源分配的最小單位。由 文本區域
,數據區域
和堆棧
組成。
- 文本區域存儲處理器執行的代碼
- 數據區域存儲變量和進程執行期間使用的動態分配的內存;
- 堆棧區域存儲着活動過程調用的指令和本地變量。
涉及問題: cpu搶佔
,內存分配(虛擬內存/物理內存)
,以及進程間通信
。
1.2、線程
線程是操作系統能夠進行運算調度的最小單位。
一個進程可以包括多個線程,線程共用
進程所分配到的資源空間
涉及問題: 線程狀態
,並發問題
,鎖
1.3、協程
子例程
: 某個主程序的一部分代碼,也就是指某個方法,函數。
維基百科:執行過程類似於 子例程
,有自己的上下文,但是其切換由自己控制。
1.4、常見問題
- 1、進程和線程的區別
進程擁有自己的資源空間,而線程需要依賴於進程進行資源的分配,才能執行相應的任務。
進程間通信需要依賴於 管道,共享內存,信號(量)和消息隊列等方式。
線程不安全,容易導致進程崩潰等
- 2、什麼是多線程
線程是運算調度的最小單位,即每個處理器在某個時間點上只能處理一個線程任務調度。
在多核cpu 上,為了提高我們cpu的使用率,從而引出了多線程的實現。
通過多個線程任務並發調度,實現任務的並發執行。也就是我們所說的多線程任務執行。
二、Thread
2.1、使用多線程
2.1.1、繼承 Thread 類
class JayThread extends Thread{
@Override
public void run(){
System.out.println("hello world in JayThread!");
}
}
class Main{
public static void main(String[] args){
JayThread t1 = new JayThread();
t1.start();
}
}
2.1.2、實現 Runnable 接口
class JayRunnable implements Runnable{
@Override
public void run(){
System.out.println("hello world in JayRunnable!")
}
}
class Main{
public static void main(String[] args){
JayRunnable runnable = new JayRunnable();
Thread t1 = new Thread(runnable);
t1.start();
}
}
2.1.3、實現 Callable 接口
class JayCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("run in JayCallable " + Thread.currentThread().getName());
return "Jayce";
}
}
class Main{
public static void main(String[] args) {
Thread.currentThread().setName("main thread");
ThreadPoolExecutor executor =new ThreadPoolExecutor(10,20,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
Future<String> future = executor.submit(new JayCallable());
try {
future.get(10, TimeUnit.SECONDS);
}catch (Exception e){
System.out.println("任務執行超時");
}
}
}
2.1.4、常見問題
- 1、使用多線程有哪些方式
常用的方式主要由上述3種,需要注意的是 使用
,而不是創建線程,從實現的代碼我們可以看到,Java 創建線程只有一種方式, 就是通過 new Thread()
的方式進行創建線程。
- 2、
Thread()
,Runnable()
與Callable()
之間的區別
Thread
需要繼承,重寫run()
方法,對拓展不友好,一個類即一個線程任務。
Runnbale
通過接口的方式,可以實現多個接口,繼承父類。需要創建一個線程進行裝載任務執行。
Callable
JDK1.5 後引入, 解決 Runnable 不能返回結果或拋出異常的問題。需要結合 ThreadPoolExecutor
使用。
- 3、
Thread.run()
和Thread.start()
的區別
Thread.run()
public static void main(String[] args){
Thread.currentThread().setName("main thread");
Thread t1 = new Thread(()->{
System.out.println("run in "+Thread.currentThread().getName());
});
t1.setName("Jayce Thread");
t1.run();
}
輸出結果:
Thread.start()
public static void main(String[] args){
Thread.currentThread().setName("main thread");
Thread t1 = new Thread(()->{
System.out.println("run in "+Thread.currentThread().getName());
});
t1.setName("Jayce Thread");
t1.start();
}
輸出結果:
start() 方法來啟動線程,使當前任務進入 cpu 等待隊列(進入就緒狀態,等待cpu分片),獲取分片後執行run方法。
run() 方法執行,會被解析成一個普通方法的調用,直接在當前線程執行。
2.2、線程狀態
線程狀態,也稱為線程的生命周期, 主要可以分為: 新建
,就緒
,運行
,死亡
,堵塞
等五個階段。
圖片引用 芋道源碼
2.2.1 新建
新建狀態比較好理解, 就是我們調用 new Thread()
的時候所創建的線程類。
2.2.2 就緒
就緒狀態指得是:
1、當調用 Thread.start
時,線程可以開始執行, 但是需要等待獲取 cpu 資源。區別於 Thread.run
方法,run
方法是直接在當前線程進行執行,沿用其 cpu 資源。
2、運行狀態下,cpu 資源
使用完後,重新進入就緒狀態,重新等待獲取 cpu 資源
. 從圖中可以看到,可以直接調用Thread.yield
放棄當前的 cpu資源,進入就緒狀態。讓其他優先級更高的任務優先執行。
2.2.3 運行
在步驟2
就緒狀態中,獲取到 cpu資源
後,進入到運行狀態, 執行對應的任務,也就是我們實現的 run()
方法。
2.2.4 結束
1、正常任務執行完成,run() 方法執行完畢
2、異常退出,程序拋出異常,沒有捕獲
2.2.5 阻塞
阻塞主要分為: io等待,鎖等待,線程等待 這幾種方式。通過上述圖片可以直觀的看到。
io等待: 等待用戶輸入,讓出cpu資源,等用戶操作完成後(io就緒),重新進入就緒狀態。
鎖等待:同步代碼塊需要等待獲取鎖,才能進入就緒狀態
線程等待: sleep()
, join()
和 wait()/notify()
方法都是等待線程狀態的阻塞(可以理解成當前線程的狀態受別的線程影響)
二、線程池
2.1 池化技術
池化技術,主要是為了減少每次資源的創建,銷毀所帶來的損耗,通過資源的重複利用提高資源利用率而實現的一種技術方案。常見的例如: 數據庫連接池,http連接池以及線程池等。都是通過池同一管理,重複利用,從而提高資源的利用率。
使用線程池的好處:
- 降低資源消耗:通過重複利用已創建的線程降低線程創建和銷毀造成的消耗。
- 提高響應速度:當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性:線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
2.2 線程池創建
2.2.1 Executors (不建議)
Executors 可以比較快捷的幫我們創建類似 FixedThreadPool ,CachedThreadPool 等類型的線程池。
// 創建單一線程的線程池
public static ExecutorService newSingleThreadExecutor();
// 創建固定數量的線程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 創建帶緩存的線程池
public static ExecutorService newCachedThreadPool();
// 創建定時調度的線程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 創建流式(fork-join)線程池
public static ExecutorService newWorkStealingPool();
存在的弊端:
FixedThreadPool 和 SingleThreadExecutor :允許請求的隊列長度為 Integer.MAX_VALUE ,可能堆積大量的請求,從而導致OOM。
CachedThreadPool 和 ScheduledThreadPool :允許創建的線程數量為 Integer.MAX_VALUE ,可能會創建大量線程,從而導致OOM。
2.2.2 ThreadPoolExecuotr
構造函數:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
幾個核心的參數:
- 1、
corePoolSize
: 核心線程數 - 2、
maximumPoolSize
: 最大線程數 - 3、
keepAliveTime
: 線程空閑存活時間 - 4、
unit
: 時間單位 - 5、
workQueue
: 等待隊列 - 6、
threadFactory
: 線程工廠 - 7、
handler
: 拒絕策略
與上述的 ExecutorService.newSingleThreadExecutor
等多個api
進行對比,可以比較容易的區分出底層的實現是依賴於 BlockingQueue
的不同而定義的線程池。
主要由以下幾種的阻塞隊列:
- 1、
ArrayBlockingQueue
,隊列是有界的,基於數組實現的阻塞隊列 - 2、
LinkedBlockingQueue
,隊列可以有界,也可以無界。基於鏈表實現的阻塞隊列 對應了:Executors.newFixedThreadPool()
的實現。 - 3、
SynchronousQueue
,不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作將一直處於阻塞狀態。對應了:Executors.newCachedThreadPool()
的實現。 - 4、
PriorityBlockingQueue
,帶優先級的無界阻塞隊列
拒絕策略主要有以下4種:
- 1、
CallerRunsPolicy
: 在調用者線程執行 - 2、
AbortPolicy
: 直接拋出RejectedExecutionException異常 - 3、
DiscardPolicy
: 任務直接丟棄,不做任何處理 - 4、
DiscardOldestPolicy
: 丟棄隊列里最舊的那個任務,再嘗試執行當前任務
2.3 線程池提交任務
往線程池中提交任務,主要有兩種方法,execute()
和submit()
1、 execute()
無返回結果,直接執行任務
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> System.out.println("hello"));
}
2、submit()
submit()
會返回一個 Future
對象,用於獲取返回結果,常用的api 有 get()
和 get(timeout,unit)
兩種方式,常用於做限時處理
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
System.out.println("hello world! ");
return "hello world!";
});
System.out.println("get result: " + future.get());
}
三、線程工具類
3.1 ThreadlLocal
ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal為變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。
3.2 Semaphore
Semaphore ,是一種新的同步類,它是一個計數信號. 使用示例代碼:
// 線程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5個線程同時訪問
final Semaphore semp = new Semaphore(5);
// 模擬20個客戶端訪問
for (int index = 0; index < 50; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 獲取許可
semp.acquire();
System.out.println("Accessing: " + NO);
Thread.sleep((long) (Math.random() * 6000));
// 訪問完後,釋放
semp.release();
//availablePermits()指的是當前信號燈庫中有多少個可以被使用
System.out.println("-----------------" + semp.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
exec.execute(run);
}
// 退出線程池
exec.shutdown();
3.3 CountDownLatch
可以理解成是一個柵欄,需要等所有的線程都執行完成後,才能繼續往下走。
CountDownLatch
默認的構造方法是 CountDownLatch(int count)
,其參數表示需要減少的計數,主線程調用 #await()
方法告訴 CountDownLatch
阻塞等待指定數量的計數被減少,然後其它線程調用 CountDownLatch
的 #countDown()
方法,減小計數(不會阻塞)。等待計數被減少到零,主線程結束阻塞等待,繼續往下執行。
3.4 CyclicBarrier
CyclicBarrier
與 CountDownLatch
有點相似, 都是讓線程都到達某個點,才能繼續往下走, 有所不同的是 CyclicBarrier
是可以多次使用的。 示例代碼:
CyclicBarrier barrier;
public TaskThread(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(getName() + " 到達柵欄 A");
barrier.await();
System.out.println(getName() + " 衝破柵欄 A");
Thread.sleep(2000);
System.out.println(getName() + " 到達柵欄 B");
barrier.await();
System.out.println(getName() + " 衝破柵欄 B");
} catch (Exception e) {
e.printStackTrace();
}
}
四、總結
最後貼一個新生的公眾號 (Java 補習課
),歡迎各位關注,主要會分享一下面試的內容(參考之前博主的文章),阿里的開源技術之類和阿里生活相關。 想要交流面試經驗的,可以添加我的個人微信(Jayce-K
)進群學習~