一、基本概念
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
)進群學習~