面試【JAVA基礎】多線程


本次整理的內容以下:java

java基礎之線程池.png

一、進程與線程的區別

進程是一個可執行的程序,是系統資源分配的基本單位;線程是進程內相對獨立的可執行單元,是操做系統進行任務調度的基本單位。算法

二、進程間的通訊方式

2.一、操做系統內核緩衝區

因爲每一個進程都有獨立的內存空間,進程之間的數據交換須要經過操做系統內核。須要在操做系統內核中開闢一塊緩衝區,進程 A 將須要將數據拷貝到緩衝區中,進程 B 從緩衝區中讀取數據。由於共享內存沒有互斥訪問的功能,需配合信號量進行互斥訪問。編程

2.二、管道

管道的實現方式:小程序

  • 父進程建立管道,獲得兩個描述文件指向管道的兩端。
  • 父進程 fork 出子進程,子進程也擁有兩個描述文件,指向同一個管道的兩端。
  • 父進程關閉讀端(fd(0)),子進程關閉寫端(fd(1))。父進程往管道里面寫,子進程從管道里面讀。

管道的特色:
只容許具備血緣關係的進程間通信,只容許單向通信,進程在管道在,進程消失管道消失。管道內部經過環形隊列實現。
有名管道(命名管道):
經過文件的方式實現進程間的通訊。容許無血緣關係的進程間的通訊數組

2.三、消息隊列

由消息組成的鏈表,存在系統內核中。克服了信號量傳遞的信息少,管道只能承載無格式的字符流及緩衝區的大小受限等特色。經過消息類型區分消息。服務器

2.四、信號量

本質是一個計數器,不以傳送數據爲目的,主要用來保護共享資源,使得資源在一個時刻只有一個進程獨享。多線程

2.五、套接字

可用於不一樣機器間進程的通訊。
套接字包括 3 個屬性:域、類型、 協議。併發

  • 域包括 ip 端口
  • 類型指的是兩種通訊機制:流(stream)和數據報(datagram)
  • 協議指 TCP/UDP 底層傳輸協議

建立 socket 經過 bind 命名綁定端口,listen 建立隊列保存未處理的客戶端請求,accept 等待客戶端的鏈接,connect 服務端鏈接客戶端 socket,close 關閉服務端客戶端的鏈接。異步

stream 和 datagram 的區別:
stream 能提供有序的、可靠的、雙向的、基於鏈接的字節流(TCP),會有拆包粘包問題。
datagram 是無鏈接、不可靠、使用固定大小的緩衝區的數據報服務(UDP),由於基於數據報,且有固定的大小,因此不會有拆包粘包問題。socket

詳細請參考:進程間的五種通訊方式介紹

三、線程間的通訊方式

共享內存:
Java 採用的就是共享內存,內存共享方式必須經過鎖或者 CAS 技術來獲取或者修改共享的變量,看起來比較簡單,可是鎖的使用難度比較大,業務複雜的話還有可能發生死鎖。
消息傳遞:
Actor 模型便是一個異步的、非阻塞的消息傳遞機制。Akka 是對於 Java 的 Actor 模型庫,用於構建高併發、分佈式、可容錯、事件驅動的基於 JVM 的應用。消息傳遞方式就是顯示的經過發送消息來進行線程間通訊,對於大型複雜的系統,可能優點更足。

詳細請參考:Java 內存模型分析

四、多線程的優缺點

優勢:
充分利用 cpu 的資源,提升 cpu 的使用率,使程序的運行效率提升。
缺點:
有大量的線程會影響性能,操做系統會在線程之間切換,會增長內存的開銷。可能會產生死鎖、存在線程之間的併發問題。

五、建立線程的方法

  1. 集成 Thread 類,重寫 run 方法,利用 start 啓動線程。
  2. 實現 Runable 接口建立線程,重寫 run 方法,經過 new Thread 方式建立線程。
  3. 經過 callable 和 futuretask 建立線程,實現 callable 接口,重寫 call 方法,使用 future 對象包裝 callable 實例,經過 new Thread 方式建立線程。
  4. 經過線程池建立線程。

六、runable 和 callable 區別

  1. runable 是重寫 run 方法,callable 重寫 call 方法。
  2. runable 沒有返回值,callable 有返回值。
  3. callable 中的 call 方法能夠拋出異常,runable 中的 run 方法不能向外界拋出異常。
  4. 加入線程池運行 runable 使用 execute 運行,callable 使用 submit 方法。

七、sleep 和 wait 區別

  1. wait 只能在 synchronized 塊中調用,屬於對象級別的方法,sleep 不須要,屬於 Thread 的方法。
  2. 調用 wait 方法時候會釋放鎖,sleep 不會釋放鎖。
  3. wait 超時以後線程進入就緒狀態,等待獲取 cpu 繼續執行。

八、yield 和 join 區別

  1. yield 釋放 cpu 資源,讓線程進入就緒狀態,屬於 Thread 的靜態方法,不會釋放鎖,只能使同優先級或更高優先級的線程有執行的機會。
  2. join 等待調用 join 方法的線程執行完成以後再繼續執行。join 會釋放鎖和 cpu 的資源,底層是經過 wait 方法實現的。

九、死鎖的產生條件

  1. 互斥條件。
  2. 請求與保持條件。
  3. 不可剝奪條件。
  4. 循環等待條件。

詳細請參考:併發編程挑戰:死鎖與上下文切換

十、如何解決死鎖

  1. 破壞請求與保持條件
    靜態分配,每一個線程開始前就獲取須要的全部資源。
    動態分配,每一個線程請求獲取資源時自己不佔有資源。
  2. 破壞不可剝奪條件
    當一個線程不能獲取全部的資源時,進入等待狀態,其已經獲取的資源被隱式釋放,從新加入到系統的資源列表中,可被其餘線程使用。
  3. 死鎖檢測:銀行家算法

十一、threadLocal 的實現

  1. ThreadLocal 用於提供線程局部變量在多線程環境下能夠保證各個線程裏面的變量獨立於其餘線程裏的變量。
  2. 底層使用 ThreadLocalMap 實現,每一個線程都擁有本身的 ThreadLocalMap,內部爲繼承了 WeakReference 的 Entry 數組,包含的 Key 爲 ThreadLocal,值爲 Object。

詳細請參考:【SharingObjects】ThreadLocal

十二、threadLocal 何時會發生內存泄漏

java.lang.ThreadLocal.ThreadLocalMap.Entry:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                //重點!!!!!
                super(k);
                value = v;
            }
        }

由於 ThreadLocalMap 中的 key 是弱引用,而 key 指向的對象是 threadLocal,一旦把 threadLocal 實例置爲 null 以後,沒有任何強引用的對象指向 threadLocal 對象,所以 threadLocal 對象會被 Gc 回收,但與之關聯的 value 卻不能被回收,只有當前線程結束後,對應的 map value 纔會被回收。若是當前線程沒結束,可能會致使內存泄漏。
如線程池的場景,在線程中將 threadlocal 置爲 null,但線程沒被銷燬且一直不被使用,就可能會致使內存泄漏

在調用 get、set、remove 方法時,會清除線程 map 中全部 key 爲 null 的 value。因此在不使用 threadLocal 時調用 remove 移除對應的對象。

1三、線程池

13.一、線程池類結構

ThreadPoolExecutor 繼承關係圖:
ThreadPoolExecutor.png

13.二、shutDown 和 shutDownNow 的區別、

shutDown 方法執行以後會變成 SHUTDOWN 狀態,沒法接受新任務,隨後等待已提交的任務執行完成。
shutDownNow 方法執行以後變成 STOP 狀態,沒法接受新任務。並對執行中的線程執行 Thread.interrupt()方法。

  • SHUTDOWN:不接受新任務提交,可是會繼續處理等待隊列中的任務。
  • STOP:不接受新任務提交,再也不處理等待隊列中的任務,中斷正在執行任務的線程。
13.三、線程池的參數
  1. CorePoolSize 核心線程數
  2. MaximumPoolSize 最大線程數,線程池容許建立的最大線程數
  3. keepAliveTime 空閒線程的存活時間
  4. wokeQueue 任務隊列
  5. handler 飽和策略
  6. threadFactory 用於生成線程。

當任務來時,若是當前的線程數到達核心線程數,會將任務加入阻塞隊列中,若是阻塞隊列滿了以後,會繼續建立線程直到線程數量達到最大線程數,若是線程數量已經達到最大線程數量,且任務隊列滿了以後,會執行拒絕策略。

若是想讓核心線程被回收,可使用 allowCoreThreadTimeOut 參數,若是爲 false(默認值),核心線程即便在空閒時也保持活動狀態。若是 true,核心線程使用 keepAliveTime 來超時等待工做。

13.四、線程池的飽和策略
  1. CallerRunsPolicy:由提交任務的線程本身執行這個任。
  2. AbortPolicy (默認): 直接拋出 RejectExecutionException 異常。
  3. DisCardPolicy:不作處理,拋棄掉當前任務。
  4. DiscardOldestPolicy: 把隊列隊頭的任務直接扔掉,提交當前任務進阻塞隊列。
13.五、線程池分類

java.util.concurrent.Executors 類:

  1. newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

生成一個固定大小的線程池,此時核心線程數和最大線程數相等,keepAliveTime = 0 ,任務隊列採起 LinkedBlockingQueue 無界隊列(也可設置爲有界隊列)。
適用於爲了知足資源管理需求,而須要限制當前線程數量的應用場景,好比負載比較重的服務器。

  1. newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));

生成只有一個線程的線程池,核心線程數與最大線程數都是 1,keepAliveTime = 0,任務隊列採起 LinkedBlockingQueue,適用於須要保證順序地執行各個任務,而且在任意時間點不會有多個線程是活動的應用場景。

  1. newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

核心線程數是 0,最大線程數是 int 最大值,keepaliveTime 爲 60 秒,任務隊列採起 SynchronousQueue,適用於執行不少的短時間異步任務的小程序,或者是負載較輕的服務器。

  1. newScheduledThreadPool
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

定長的線程池,支持週期性任務,最大線程數是 int 最大值,超時時間爲 0,任務隊列使用 DelayedWorkQueue,適用於須要多個後臺執行週期任務,同時爲了知足資源管理需求而須要限制後臺線程的數量的應用場景。

13.六、任務執行過程當中出現異常會怎麼樣?

任務執行失敗後,只會影響到當前執行任務的線程,對於整個線程池是沒有影響的。

詳細請參考:ThreadPoolExecutor 線程池任務執行失敗的時候會怎樣

13.七、線程池的底層實現
  1. 使用 hashSet 存儲 worker
  2. 每一個 woker 控制本身的狀態
  3. 執行完任務以後循環獲取任務隊列中的任務
13.八、重啓服務、如何優雅停機關閉線程池

kill -9 pid 操做系統內核級別強行殺死某個進程。
kill -15 pid 發送一個通知,告知應用主動關閉。

ApplicationContext 接受到通知以後,會執行 DisposableBean 中的 destroy 方法。
通常咱們在 destroy 方法中作一些善後邏輯。
調用 shutdown 方法,進行關閉。

13.九、爲何使用線程池
  1. 下降資源消耗,減小建立銷燬線程的成本。
  2. 提升響應速度。
  3. 提升線程的可管理性,線程的無限制的建立,消耗系統資源,下降系統的穩定性。

相關文章
相關標籤/搜索