《面試補習》- 多線程知識梳理

一、基本概念

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)進群學習~