《面試補習》- 多執行緒知識梳理

jaycekong發表於2021-06-28

一、基本概念

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

CyclicBarrierCountDownLatch 有點相似, 都是讓執行緒都到達某個點,才能繼續往下走, 有所不同的是 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)進群學習~

相關文章