Java 面試必會知識點:Java 多執行緒與併發程式設計

GitChat技術雜談發表於2017-11-25

640.gif?wxfrom=5&wx_lazy=1

本文來自作者 張振華 在 GitChat 上分享「Java 工程師面試/工作必知必會:Java 多執行緒與併發程式設計」,閱讀原文」檢視交流實錄

文末高能

編輯 | 雷諾

一、Java-Thread 概念

我們想搞懂多執行緒必須先明白以下幾個重要概念。

  1. 什麼是程式

    是資源分配的最小單位;(資源,包括各種表格、記憶體空間、磁碟空間) 同一程式中的多條執行緒將共享該程式中的全部系統資源。

  2. 什麼是執行緒

    執行緒是CPU排程的最小單位。執行緒只由相關堆疊(系統棧或使用者棧)暫存器和執行緒控制表組成。 而暫存器可被用來儲存執行緒內的區域性變數。

  3. 什麼是並行和併發

  • 並行執行:匯流排程數<=CPU數量*核心數。

  • 併發執行:匯流排程數>CPU數量*核心數。 (如:有的作業系統CPU執行緒切換之間用的時間片輪轉程式排程演算法)

執行緒建立的4個方法大家想一下。

二、安全和鎖

Java 裡面如果談到執行緒,最核心要搞明白的就是執行緒安全和執行緒鎖的問題。

1. 何為安全

我先問一下各位小夥伴什麼叫執行緒安全或者是不安全的?思考一下:何為安全???思考2分鐘。

我總結出來的一個定理啊,一定要銘記:

Jack定理1:

離開單例、全域性共享變數來談執行緒安全問題都是耍流氓。

那麼問題來了?單例的一定是執行緒不安全的嗎?答案是否定的,只要你單例的類裡面沒有全域性變數那一定是執行緒安全的。

所以只有單例模式下共享全域性變數的時候才會有執行緒不安全的問題,這個時候我們就要引入鎖的概念了。

2. 鎖

經常在工作中聽到我們的小夥伴們談論什麼樂觀鎖,悲觀鎖,排它鎖,共享鎖等等,但記住這些只是結果。在我們Java中我認為只有兩種鎖:隱式鎖和顯示鎖兩種實現手段。

隱式鎖: synchronized

  1. 同一個物件鎖下面的, synchronized 區域是互斥的

  2. 方法鎖(預設是當前物件的鎖)

  3. 程式碼快鎖(效能高於方法鎖,可以指定哪個物件的鎖)

Jack 定理2:

離開物件來談 synchronized,也是耍流氓。synchronized 一定是加在物件上的切記。

使用方法案例:

public synchronized void updateUser(UserInfo userInfo){    。。。。//共享資料操作 } public  void updateUser(UserInfo userInfo){    synchronized(this) {    。。。。//共享資料操作    } }

顯示鎖:java.util.concurrent.lock  

  1. 需要手動關/開

  2. 注意自己的程式碼邏輯不要產生死鎖了

使用案例:

public void updateUser(UserInfo userInfo){    Lock lock = new ReentrantLock();    lock.lock();//加鎖    try {        。。。。//共享資料操作    } finally {        lock.unlock();//釋放鎖,一定要釋放    } }

3. synchronized 與 lock 區別

  • lock更靈活,方法更多,能實現各種鎖的場景。

  • 效能上如果都指定鎖都是一個物件,那基本上沒什麼差別。

  • 預設情況下synchronized鎖是當前物件,而lock是不一樣的。

三、Concurrent 包

java.util.concurrent 包是必須要了解的,如果你不知道有這個包的存在就別談多執行緒。

我們可以把這個包下面的內容分成四部分

1. 原子性操作類

原子操作(atomic operation)是不需要 synchronized,也可以實現多執行緒的安全,效率要比 lock 高很多。

底層是通過一定的演算法將記憶體中分割了一個獨立排它的記憶體空間,來做單執行緒操作。目前只有一些AtomicBoolean、AtomicInteger、AtomicLong等一些基本型別。

2. 執行緒佇列

我們學習資料結構的時候都知道有棧和佇列兩種結構,而Java給我提供了一些執行緒安全的佇列操作的類。

0?wx_fmt=jpeg

而其中關鍵的幾個類,我們大概介紹一下:

0?wx_fmt=jpeg

  • BlockingQueue 很好的解決了多執行緒中高效安全“傳輸”資料的問題;基於 java.util.Queue 的基礎上做了一些執行緒安全的封裝;

  • ArrayBlockingQueue 基於陣列的阻塞佇列實現,在ArrayBlockingQueue內部,維護了一個定長陣列,以便快取佇列中的資料物件,這是一個常用的阻塞佇列,除了一個定長陣列外,ArrayBlockingQueue內部還儲存著兩個整形變數,分別標識著佇列的頭部和尾部在陣列中的位置。

  • LinkedBlockingQueue 基於連結串列的阻塞佇列,同ArrayListBlockingQueue類似,其內部也維持著一個資料緩衝佇列。

  • DelayQueue 中的元素只有當其指定的延遲時間到了,才能夠從佇列中獲取到該元素。

    DelayQueue 是一個沒有大小限制的佇列,因此往佇列中插入資料的操作(生產者)永遠不會被阻塞,而只有獲取資料的操作(消費者)才會被阻塞。

    使用場景:DelayQueue 使用場景較少,但都相當巧妙,常見的例子比如使用一個 DelayQueue 來管理一個超時未響應的連線佇列。

  • PriorityBlockingQueue 基於優先順序的阻塞佇列(優先順序的判斷通過建構函式傳入的 Compator 物件來決定)。

    但需要注意的是 PriorityBlockingQueue 並不會阻塞資料生產者,而只會在沒有可消費的資料時,阻塞資料的消費者。

  • SynchronousQueue 一種無緩衝的等待佇列,同步佇列沒有任何內部容量,甚至連一個佇列的容量都沒有;

    其中每個 put 必須等待一個 take,反之亦然。無鎖的機制實現(可想而知高併發的時候效能肯定是最高的)。

關於佇列只介紹個大概,大家知道有這麼回事,具體使用可以查詢相關的API文件。為什麼要提一下呢,因為我們在說明執行緒池的時候有用到安全佇列。

3. 執行緒閥

執行緒閥:控制執行緒的開(開始)與關(結束)。如果用Queue來管理執行緒的佇列即開始,那麼用執行緒閥管理整體執行緒的調配工作,即執行緒結束之後的開與關。我們這裡大概介紹4個類:

  1. CountDownLatch 是通過一個計數器來實現的,計數器的初始值為執行緒的數量。每當一個執行緒完成了自己的任務後,計數器的值就會減1。

    當計數器值到達0時,它表示所有的執行緒已經完成了任務,然後在閉鎖上等待的執行緒就可以恢復執行任務。

  2. CyclicBarrier是一個同步輔助類,它允許一組執行緒互相等待,直到到達某個公共屏障點 (common barrier point)。

    在涉及一組固定大小的執行緒的程式中,這些執行緒必須不時地互相等待,此時 CyclicBarrier 很有用。

    因為該 barrier 在釋放等待執行緒後可以重用,所以稱它為迴圈 的 barrier。

  3. Semaphore:一個計數訊號量。從概念上講,訊號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release() 新增一個許可,從而可能釋放一個正在阻塞的獲取者。就像一個排隊進入上海博物館一樣,放幾個人等一下,有幾個人走了然後再放幾個人進入,像是一種排隊機制。

  4. Future->FutureTask:一般FutureTask多用與耗時的計算,主執行緒再完成自己的任務後,再去獲取結果。只有在計算完成時獲取,否則會一直阻塞直到任務完成狀態。

具體語法和使用可以查詢相關文件。

4.  Java 提供的執行緒安排工具類

java.util.concurrent.ConcurrentHashMap   java.util.concurrent.ConcurrentLinkedQueue   java.util.concurrent.ConcurrentMap   java.util.concurrent.ConcurrentNavigableMap   java.util.concurrent.ConcurrentSkipListMap   java.util.concurrent.ConcurrentSkipListSet

……..等等基於lock的演算法實現

5.  volatile關鍵字

我們通過檢視原始碼,會發現 java 的另外一個關鍵字volatile,執行緒在每次使用變數的時候,都會讀取變數修改後的最的值。(其實是有風險的,並行情況下不一定正確,有可能兩個執行緒同時取到最後修改的值)

四、執行緒池

1. 執行緒池要解決的問題:

我們掌握執行緒池必須要明白執行緒池要接解決的兩個問題:

  • 解決頻繁建立執行緒所產生的開銷。減少在建立和銷燬執行緒上所花的時間以及系統資源的開銷。

  • 解決無限制的建立執行緒引起的系統崩潰。如不使用執行緒池,有可能造成系統建立大量執行緒而導致消耗完系統記憶體以及“過度切換”  

2. Executors給我們提供的四種建立執行緒池的方法

建立一個可重用固定執行緒數的執行緒池  

ExecutorService pool = Executors.newFixedThreadPool(5);

newFixedThreadPool的引數指定了可以執行的執行緒的最大數目,超過這個數目的執行緒加進去以後,不會立馬執行,會做佇列等待。其次,加入執行緒池的執行緒屬於託管狀態,執行緒的執行不受加入順序的影響

單任務執行緒池

ExecutorService pool = Executors.newSingleThreadExecutor();

一個一個執行,這種基本上很少用到。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

可變尺寸的執行緒池

ExecutorService pool = Executors.newCachedThreadPool();

如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。

定時以及週期性執行任務的執行緒池  

ScheduledThreadPoolExecutor exec = Executors.ScheduledThreadPoolExecutor(1);        exec.scheduleAtFixedRate(new Runnable() {                      publicvoid run() { .....////每隔一段時間就觸發的執行緒內容                      }                  }, 1000, 5000,TimeUnit.MILLISECONDS);

3. 自定義執行緒池

我們檢視Executors的原始碼發現底層都是呼叫ThreadPoolExecutor來實現的,裡面有幾個重要引數我們一定要記一下:

public static ExecutorService newCachedThreadPool() {        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue<Runnable>());    }    public static ExecutorService newFixedThreadPool(int nThreads) {        return new ThreadPoolExecutor(nThreads, nThreads,                                      0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue<Runnable>());    }      //ThreadPoolExecutor構造器    public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {        if (corePoolSize < 0 ||            maximumPoolSize <= 0 ||            maximumPoolSize < corePoolSize ||            keepAliveTime < 0)            throw new IllegalArgumentException();        if (workQueue == null || threadFactory == null || handler == null)            throw new NullPointerException();        this.corePoolSize = corePoolSize;        this.maximumPoolSize = maximumPoolSize;        this.workQueue = workQueue;        this.keepAliveTime = unit.toNanos(keepAliveTime);        this.threadFactory = threadFactory;        this.handler = handler;    }

  • corePoolSize:池中所儲存的執行緒數,包括空閒執行緒。

  • maximumPoolSize:池中允許的最大執行緒數。

  • keepAliveTime: 當執行緒數大於核心時,此為終止前多餘的空閒執行緒等待新任務的最長時間。

  • unit: keepAliveTime引數的時間單位。

  • BlockingQueue: 執行前用於保持任務的佇列。此佇列僅保持由execute方法提交的Runnable任務。常見的三種佇列:

    • 直接提交。SynchronousQueue。

    • 無界佇列。使用無界佇列(例如,不具有預定義容量的LinkedBlockingQueue)

    • 有界佇列。當使用有限maximumPoolSizes時,有界佇列(如ArrayBlockingQueue)有助於防止資源耗盡。

  • ThreadFactory: 執行程式建立新執行緒時使用的工廠。預設情況下為Executors.defaultThreadFactory():我們可以採用自定義的ThreadFactory工廠,增加對執行緒建立與銷燬等更多的控制。

  • RejectedExecutionHandler: (拒絕策略)由於超出執行緒範圍和佇列容量而使執行被阻塞時所使用的處理程式。

    • AbortPolicy(預設):這種策略直接丟擲異常,丟棄任務。

    • DiscardPolicy:不能執行的任務將被刪除;這種策略和AbortPolicy幾乎一樣,也是丟棄任務,只不過他不丟擲異常。

    • DiscardOldestPolicy:如果執行程式尚未關閉,則位於工作佇列頭部的任務被刪除,然後重試執行程式(如果再次失敗,則重複此過程)。

    • CallerRunsPolicy: 使用此策略,如果新增到執行緒池失敗,那麼主執行緒會自己去執行該任務,不會等待執行緒池中的執行緒去執行。就像是個急脾氣的人,我等不到別人來做這件事就乾脆自己幹。

    • 當然也可以自定義。

Jack定理3:

離開全域性和單例談論執行緒池那也是耍流氓。工作中看到有人把執行緒池寫在方法裡面的區域性變數,那有用嗎?

4. 執行緒的監控和分析方法

VisualVM的使用

VisualVM 是 JDK 的一個整合的分析工具,自從JDK 6 Update 7以後已經作為 Sun 的 JDK 的一部分。

VisualVM 可以做的:監控應用程式的效能和記憶體佔用情況、監控應用程式的執行緒、進行執行緒轉儲(Thread Dump)或堆轉儲(Heap Dump)、跟蹤記憶體洩漏、監控垃圾回收器、執行記憶體和CPU分析,儲存快照以便離線分析應用程式。

0?wx_fmt=png

Jconsole的使用

JConsole 是一個內建 Java 效能分析器,可以從命令列或在 GUI shell 中執行。

0?wx_fmt=png

在Java Visualvm工具裡面安裝JTA外掛

0?wx_fmt=png

利用linux的top&jstack命令

例如:top先找到Java程式,top -p 8442 -H 找到哪個執行緒,jstack 8442> ./8442_dump.txt輸出thread的demp檔案。

在實際生產環境,一般我們都是自己公司的監控平臺的,只需要到各大監控平臺開 thread 即可,內容基本上一樣。

Jack 定理4:

任何 Java 執行的類和相關資訊都在堆疊裡面,就是我們如何想辦法看到他們的問題,萬變不離其宗。

五、執行緒和執行緒池工作中的應用場景:

  1. ervlet 我們java開發最基本的東西,其啟動的時候其實是開闢了一個main執行緒的。

    而其中 servlet 類是單例的所以它是執行緒不安全的,但是在沒有共享全域性變數的情況,而 reqest 和 response 是一個請求是一個例項,而其本身的資料設計又是執行緒安全的。

  2. Tomcat Servlet 的容器 tomcat 其實是對執行緒的執行緒池做了控制的。提高請求處理效率和避免請求太多把容器弄掛。

  3. Spring 預設載入 bean 的方式是單例的,所以其是執行緒不安全的。

  4. 資料庫連線池,其實也是多執行緒。

  5. nginx 前端閘道器請求,也是利用了執行緒池的原理。

  6. 而我們的客戶端ios,android其實也都是有主執行緒和子執行緒的說法,如果你能很好的將器裡面的執行緒掌握基本上此種客戶端開發就能掌握一半。

Jack定理5:

執行緒無處不在,執行緒池也無處不在,只不過是換不同的馬甲,不通形式存在就看你知道不知道。

Jack一句話總結:

Java執行緒是圍繞著java的程式的共享記憶體的管理和資料訪問,而圍繞執行緒本身的管理產生了通訊,爭搶和佇列管理的執行緒池。

六、開放性問題?

  1. 執行緒安全和資料庫資料執行緒安全是一回事嗎?

  2. 我們實際工作中服務的最大併發量是多少?為什麼?

  3. 除了資料庫連線池,我們還有在哪些地方用過執行緒池?

  4. 你面試的時候多執行緒你都被問到了哪些問題?

以上內容是對《Java併發程式設計從入門到精通》本書的內容的高度概括。大家可以來討論歡迎留言!

近期熱文

沉迷前端,無法自拔的人,如何規劃職業生涯?

區塊鏈在哪些案例上發揮著重大作用

Java 實現 Web 應用中的定時任務

業務團隊如何高效實施自動化測試

入行 AI,如何選個腳踏實地的崗位


《GitChat 達人課:Gradle 從入門到實戰

0?wx_fmt=jpeg

「閱讀原文」看交流實錄,你想知道的都在這裡


相關文章