Java併發面試題

jcjcjcjiangcheng發表於2020-10-07

執行緒與程式區別

主要區別

  1. 程式是一個“執行中的程式”,是系統進行資源分配和排程的一個獨立單位
  2. 執行緒是程式的一個實體,一個程式中一般擁有多個執行緒。執行緒之間共享地址空間和其它資源(所以通訊和同步等操作,執行緒比程式更加容易)
  3. 執行緒一般不擁有系統資源,但是也有一些必不可少的資源(使用ThreadLocal儲存)
  4. 執行緒上下文的切換比程式上下文切換要快很多。

執行緒上下文切換比程式上下文切換快的原因

  1. 程式切換時:涉及到當前程式的CPU環境的儲存和新被排程執行程式的CPU環境的設定
  2. 執行緒切換時,僅需要儲存和設定少量的暫存器內容,不涉及儲存管理方面的操作

執行緒通訊同步有幾種方式

執行緒通訊

  1. 使用全域性變數:主要由於多個執行緒可能更改全域性變數,因此全域性變數最好宣告為volatile。
  2. 使用訊息實現通訊:每一個執行緒都可以擁有自己的訊息佇列(UI執行緒預設自帶訊息佇列和訊息迴圈,工作執行緒需要手動實現訊息迴圈),因此可以採用訊息進行執行緒間通訊sendMessage,postMessage。
  3. 使用事件CEvent類實現執行緒間通訊:Event物件有兩種狀態分別是有訊號和無訊號,執行緒可以監視處於有訊號狀態的事件,以便在適當的時候執行對事件的操作。

執行緒同步方式

  1. 臨界區

    保證在某一時刻只有一個執行緒能訪問資料的簡便辦法。在任意時刻只允許一個執行緒對共享資源進行訪問。如果有多個執行緒試圖同時訪問臨界區,那麼 在有一個執行緒進入後其他所有試圖訪問此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操 作共享資源的目的。 僅能在同一程式內使用

  2. 互斥量

    只有擁有互斥物件的執行緒才具有訪問資源的許可權,由於互斥物件只有一個,因此就決定了任何情況下此共享資源都不會同時被多個執行緒所訪問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交出,以便其他執行緒在獲得後得以訪問資源。互斥量比臨界區複雜。因為使用互斥不僅僅能夠在同一應用程式不同執行緒中實現資源的安全共享,而且可以在不同應用程式的執行緒之間實現對資源的安全共享。

  3. 訊號量

    訊號允許多個執行緒同時使用共享資源 ,這與作業系統中的PV操作相同。

  4. 事件

    事件機制,則允許一個執行緒在處理完一個任務後,主動喚醒另外一個執行緒執行任務。

執行緒之間制約關係

  1. 直接制約關係:即一個執行緒的處理結果,為另一個執行緒的輸入,因此執行緒之間直接制約著,這種關係可以稱之為同步關係
  2. 間接制約關係:即兩個執行緒需要訪問同一資源,該資源在同一時刻只能被一個執行緒訪問,這種關係稱之為執行緒間對資源的互斥訪問,某種意義上說互斥是一種制約關係更小的同步

程式通訊同步有幾種方式

  1. 通過使用套接字Socket來實現不同機器間的程式通訊
  2. 通過對映一段可以被多個程式訪問的共享記憶體來進行通訊
  3. 通過寫程式和讀程式利用管道進行通訊

多執行緒共享資料

  1. 每個執行緒執行流程相同,使用同一個Runnable物件,Runnable物件中有共享資料

    public class Main {
    
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
            new Thread(ticket).start();
            new Thread(ticket).start();
        }
    
    
    }
    class Ticket implements Runnable {
        private int ticket = 10;
    
        @Override
        public synchronized void run() {
            while (ticket > 0) {
                ticket--;
                System.out.println("當前執行緒視窗" + Thread.currentThread().getName() + "剩餘票為: " + ticket);
            }
        }
    }
    
    /**
    當前執行緒視窗Thread-0剩餘票為: 9
    當前執行緒視窗Thread-0剩餘票為: 8
    當前執行緒視窗Thread-0剩餘票為: 7
    當前執行緒視窗Thread-0剩餘票為: 6
    當前執行緒視窗Thread-0剩餘票為: 5
    當前執行緒視窗Thread-0剩餘票為: 4
    當前執行緒視窗Thread-0剩餘票為: 3
    當前執行緒視窗Thread-0剩餘票為: 2
    當前執行緒視窗Thread-0剩餘票為: 1
    當前執行緒視窗Thread-0剩餘票為: 0
    /
    
  2. 每個執行緒執行流程不相同,使用不同的Runnable物件

    public class Main {
    
        public static void main(String[] args) {
            ShareData data = new ShareData();
            new Thread(new Runnable1(data)).start();
            new Thread(new Runnable2(data)).start();
        }
    }
    
    class Runnable1 implements Runnable {
    
        private ShareData data;
    
        public Runnable1(ShareData data) {
            this.data = data;
        }
    
        @Override
        public void run() {
                data.increment();
        }
    }
    
    class Runnable2 implements Runnable {
    
        private ShareData data;
    
        public Runnable2(ShareData data) {
            this.data = data;
        }
    
        @Override
        public void run() {
                data.decrement();
        }
    }
    class ShareData {
    
        private int j = 10;
    
        public synchronized void increment() {
            j++;
            System.out.println("執行緒:" + Thread.currentThread().getName() + "加操作之後,j = " + j);
        }
    
        public synchronized void decrement() {
            j--;
            System.out.println("執行緒:" + Thread.currentThread().getName() + "減操作之後,j = " + j);
        }
    }
    
    /**
    執行緒:Thread-0加操作之後,j = 11
    執行緒:Thread-1減操作之後,j = 10
    /
    

訊號量和互斥量的區別

主要區別

  1. 互斥量用於執行緒的互斥,訊號量用於執行緒的同步
  2. 互斥是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。同步是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。
  3. 互斥量值只能為0/1,訊號量值可以為非負整數。
  4. 一個互斥量只能用於一個資源的互斥訪問,它不能實現多個資源的多執行緒互斥問題。訊號量可以實現多個同類資源的多執行緒互斥和同步。當訊號量為單值訊號量是,也可以完成一個資源的互斥訪問。
  5. 互斥量的加鎖和解鎖必須由同一執行緒分別對應使用,訊號量可以由一個執行緒釋放,另一個執行緒得到。

執行緒是怎麼實現的

主要有三種方式實現執行緒

在使用者空間中實現執行緒

結構圖

在這裡插入圖片描述

原理

把整個執行緒包放在使用者空間,核心對執行緒包一無所知。使用者呼叫庫函式實現執行緒,,在使用者空間管理執行緒時,每個程式有個專用的執行緒表,用來跟蹤程式中的執行緒,這些表和核心中的程式類似,它記錄著各個執行緒屬性,每個執行緒的程式計算器、堆疊、指標、暫存器和狀態等。

優點

  1. 使用者執行緒包可以在不支援執行緒的作業系統上實現,只需要呼叫函式庫實現執行緒即可。
  2. 由於執行緒表在本地,啟動執行緒比進行核心呼叫效率更高,並且不需要陷入核心,不需要本地上下文切換,也不用對記憶體進行快取記憶體進行重新整理。
  3. 允許每個程式都有自己的排程演算法。

缺點

  1. 無法實現阻塞系統呼叫
  2. 會發生缺頁中斷問題,如果有一個執行緒引起頁面故障,由於執行緒不在核心,核心不知道執行緒的存在,核心會把整個程式進行阻塞,直到磁碟IO完成為止,導致其他可以執行的執行緒也被阻塞。
  3. 一旦一個執行緒開始執行,那麼該程式中的其他執行緒不能執行,除非第一個執行緒自動放棄CPU。

在核心中實現執行緒

結構圖

在這裡插入圖片描述

原理

每個程式中沒有執行緒表,執行緒表在哪喝中。當某個執行緒希望建立一個新執行緒或者撤銷一個已有執行緒的時候,它會進行一個系統呼叫,這個系統呼叫會通過對執行緒表的更新完成執行緒建立或者撤銷工作。

優點

  1. 核心執行緒不需要任何新的,非阻塞系統的呼叫。
  2. 可以回收執行緒(當某個執行緒被撤銷,可以標誌它為不可執行)

缺點

  1. 一個多執行緒建立新的程式時,難以確定新程式時擁有與原程式相同數量的執行緒,或者是隻有一個執行緒。
  2. 當程式接收到一個訊號時候,難以確定用哪一個執行緒處理它。

混合實現

結構圖

在這裡插入圖片描述

原理

核心只識別核心級別執行緒,並對其進行排程。一些核心級執行緒會被多個使用者執行緒多路複用。內線執行緒可以建立,撤銷,排程這些使用者執行緒,每個核心級執行緒有一個可以輪流使用的使用者級執行緒集合。

優點

  1. 結合了核心實現執行緒和使用者空間實現執行緒的優點。
  2. 多路復,每個核心級執行緒有一個可以輪流使用的使用者級執行緒集合。

執行緒死鎖

死鎖是最常見的一種執行緒活性故障。死鎖的起因是多個執行緒之間相互等待對方而被永遠暫停(處於非Runnable)。死鎖的產生必須滿足如下四個必要條件:

  1. 資源互斥:一個資源每次只能被一個執行緒使用
  2. 請求和保持::一個執行緒因請求資源而阻塞時,對已獲得的資源保持不放
  3. 不可剝奪:執行緒已經獲得的資源,在未使用完之前,不能強行剝奪
  4. 迴圈等待:若干執行緒之間形成一種頭尾相接的迴圈等待資源關係

如何避免死鎖

  1. 粗鎖法:使用一個粒度粗的鎖來消除“請求與保持條件”,缺點是會明顯降低程式的併發效能並且會導致資源的浪費。
  2. 鎖排序法:比如某個執行緒只有獲得A鎖和B鎖,通過指定鎖的獲取順序,比如規定,只有獲得A鎖的執行緒才有資格獲取B鎖,按順序獲取鎖就可以避免死鎖。這通常被認為是解決死鎖很好的一種方法。
  3. 使用顯式鎖中的**ReentrantLock.try(long,TimeUnit)**來申請鎖

wait和sleep的區別

  1. wait:是Object的方法,必須與synchronized關鍵字一起使用,執行緒進入阻塞狀態,當notify或者notifyall被呼叫後,會解除阻塞。但是,只有重新佔用互斥鎖之後才會進入可執行狀態。睡眠時,會釋放互斥鎖。
  2. sleep:是Thread類的靜態方法,當前執行緒將睡眠n毫秒,執行緒進入阻塞狀態。當睡眠時間到了,會解除阻塞,進入可執行狀態,等待CPU的到來。睡眠不釋放鎖(如果有的話)。

用start()方法去執行run()方法而不是直接呼叫run()方法

start方法

用 start方法來啟動執行緒,是真正實現了多執行緒, 通過呼叫Thread類的start()方法來啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到cpu時間片,就開始執行run()方法。但要注意的是,此時無需等待run()方法執行完畢,即可繼續執行下面的程式碼。所以run()方法並沒有實現多執行緒。

run方法

run()方法只是類的一個普通方法而已,如果直接呼叫Run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是隻有一條,還是要順序執行,還是要等待run方法體執行完畢後才可繼續執行下面的程式碼。

上下文切換的種類

上下文切換的種類
執行緒切換同一程式中的兩個執行緒之間的切換
程式切換兩個程式之間的切換
模式切換在給定執行緒中,使用者模式和核心模式的切換
地址空間切換將虛擬記憶體切換到實體記憶體

執行緒有幾種狀態和它的上下文切換

Java執行緒的6種狀態

執行緒有六種狀態:

  1. NEW:執行緒建立,但沒有啟動
  2. RUNNABLE:代表執行緒正在執行或者不處於阻塞、等待狀態的可以被執行的狀態。執行緒建立後或脫離阻塞、等待狀態後進入可執行狀態。
  3. BLOCKED:代表執行緒嘗試獲得一個鎖時無法對鎖佔用,進入阻塞狀態;當該執行緒能夠獲得鎖時脫離阻塞狀態。
  4. WAITING:等待執行緒主動進入等待條件達成的狀態,可以使用join、wait、sleep方法。
  5. TIMED_WAITING:等待狀態新增計時器,當等待條件達成或計時器耗盡時脫離等待狀態。
  6. TERMINATED:執行緒任務結束或手動設定中斷標記。

作業系統的5種狀態

  1. 新建狀態(New):執行緒建立但沒有啟動
  2. 就緒狀態(Runnable):執行緒物件建立後,其他執行緒(比如 main 執行緒)呼叫了該物件 的 start ()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲取cpu的使用權 。
  3. 執行狀態(Running):就緒狀態( runnable )的執行緒獲得了cpu時間片(timeslice ),執行程式程式碼。
  4. 阻塞狀態(Blocked):執行緒放棄CPU的時間片(一直到某個條件達成),主動進入阻塞的狀態,有三種阻塞狀態,分別是
    1. 同步阻塞:執行緒由於嘗試獲得物件的同步鎖但無法取得時,進入鎖池,等待其他執行緒釋放該物件的鎖。
    2. 等待阻塞:執行緒主動放棄對物件上的鎖的佔用,進入等待物件通知的佇列。指wait方法
    3. 其他阻塞:執行緒主動進入休眠狀態,等待條件達成。指sleep、join方法或I/O請求。
  5. 執行緒死亡(Dead):執行緒 run ()、 main () 方法執行結束,或者因異常退出了 run ()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。

執行緒執行狀態圖

在這裡插入圖片描述

執行緒上下文切換

概念

上下文切換是值CPU通過分配時間片來執行任務,當一個任務的時間片用完,就會切換到另一個任務。在切換之前會儲存上一個任務的狀態,當下次再切換到該任務,就會載入這個狀態。(任務從儲存到再載入的過程就是一次上下文切換)。

執行緒切出

一個執行緒被剝奪處理器的使用權而被暫停執行,作業系統會將執行緒的進度資訊儲存到記憶體。

執行緒切入

一個執行緒被系統選中佔用處理器開始或繼續執行,作業系統需要從記憶體中載入執行緒的上下文。

上下文切換的種類

自發性上下文切換

執行緒由自身因素導致的上下文切換

  1. Thread.sleep()
  2. Object.wait()
  3. Thread.yeild()
  4. Thread.join()
  5. LockSupport.park()

非自發性上下文切換

執行緒由於執行緒排程器的原因被迫切出

  1. 執行緒的時間片用完,導致執行緒切出。
  2. 有一個比執行緒優先順序更高的執行緒需要被執行,導致執行緒切出。
  3. 虛擬機器的垃圾回收動作。

上下文切換開銷

簡接開銷

  1. 處理器快取記憶體重新載入記憶體時的開銷。
  2. 可能導致整個一級快取記憶體中的內容被沖刷,即被寫入到下一級快取記憶體或主存。

直接開銷

  1. 作業系統儲存回覆上下文所需的開銷
  2. 執行緒排程器排程執行緒的開銷

ThreadLocal是什麼

使用ThreadLocal維護變數時,其為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立的改變自己的副本,而不會影響其他執行緒對應的副本。

內部實現機制

  1. 每個執行緒內部都會維護一個類似HashMap的物件,稱為ThreadLocalMap,裡邊會包含若干了Entry(K-V鍵值對),相應的執行緒被稱為這些Entry的屬主執行緒
  2. Entry的Key是一個ThreadLocal例項,Value是一個執行緒特有物件。Entry的作用是為其屬主執行緒建立起一個ThreadLocal例項與一個執行緒特有物件之間的對應關係
  3. Entry對Key的引用是弱引用;Entry對Value的引用是強引用。

就緒狀態和阻塞狀態有什麼區別

就緒狀態

當執行緒物件建立後,其他執行緒呼叫它的start方法,其進入執行緒等待池,等待cpu使用權

阻塞狀態

執行緒放棄cpu的使用權,進入阻塞狀態直到重新進入就緒狀態,阻塞分為三種:

  1. 同步阻塞:執行緒由於嘗試獲得物件的同步鎖但無法取得時,進入鎖池,等待其他執行緒釋放該物件的鎖。
  2. 等待阻塞:執行緒主動放棄對物件上的鎖的佔用,進入等待物件通知的佇列。指wait方法
  3. 其他阻塞:執行緒主動進入休眠狀態,等待條件達成。指sleep、join方法或I/O請求。

兩者可以互相切換嗎

阻塞狀態可以通過獲得鎖、sleep()方法結束、呼叫join()的執行緒執行結束方法後可以切換到就緒狀態,但是就緒狀態要先獲取cpu時間片變成執行狀態後,才能轉換到阻塞狀態。

程式和執行緒切換開銷對比

虛擬記憶體

虛擬記憶體是作業系統為每個程式提供的一種抽象,每個程式都有屬於自己的、私有的、地址連續的虛擬記憶體,當然我們知道最終程式的資料及程式碼必然要放到實體記憶體上,那麼必須有某種機制能記住虛擬地址空間中的某個資料被放到了哪個實體記憶體地址上,這就是所謂的地址空間對映,作業系統是通關頁表來記住這種對映關係。每個程式都有自己的虛擬地址空間,程式內的所有執行緒共享程式的虛擬地址空間

程式切換和執行緒切換的對比

  • 最主要區別是程式切換涉及虛擬地址空間的切換而執行緒不涉及。由於每個程式都有自己的虛擬地址空間,而執行緒是共享虛擬地址空間。因此程式切換的時候涉及虛擬地址空間的切換,而同一個程式的執行緒切換不涉及虛擬地址空間的切換。

為什麼虛擬地址空間切換會比較耗時

每個程式都有自己的虛擬地址空間,把虛擬地址轉換為實體地址需要查詢頁表,頁表查詢是一個很慢的過程,因此通常使用Cache來快取常用的地址對映,這樣可以加速頁表查詢,這個cache就TLB(translation Lookaside Buffer,TLB本質上就是一個cache,是用來加速頁表查詢的)。由於每個程式都有自己的虛擬地址空間,那麼顯然每個程式都有自己的頁表,那麼當程式切換後頁表也要進行切換,頁表切換後TLB就失效了,cache失效導致命中率降低,那麼虛擬地址轉換為實體地址就會變慢,表現出來的就是程式執行會變慢,而執行緒切換則不會導致TLB失效,因為執行緒執行緒無需切換地址空間,因此我們通常說執行緒切換要比較程式切換塊,原因就在這裡。

Synchronized怎麼實現執行緒同步

synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java物件頭裡,每一個被鎖住的物件都會和一個monitor有關聯。

Java物件頭

以Hotspot虛擬機器為例,Hotspot的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Klass Pointer(型別指標)。

Mark Word

預設儲存物件的HashCode,分代年齡和鎖標誌位資訊。這些資訊都是與物件自身定義無關的資料,所以Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲存儘量多的資料。它會根據物件的狀態複用自己的儲存空間,也就是說在執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。

Klass Point

物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

Monitor

Monitor可以理解為一個同步工具或一種同步機制,通常被描述為一個物件。每一個Java物件就有一把看不見的鎖,稱為內部鎖或者Monitor鎖。

Monitor是執行緒私有的資料結構,每一個執行緒都有一個可用monitor record列表,同時還有一個全域性的可用列表。每一個被鎖住的物件都會和一個monitor關聯,同時monitor中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。

為什麼JDK 6之前synchronized效率低的

因為最初synchronized最初實現同步的方式是若有多個執行緒想獲取同步資源,synchronized會阻塞其他資源,只有一個執行緒能獲得鎖,等這個執行緒執行完再喚醒其他執行緒。

  • 但是阻塞或喚醒一個Java執行緒需要作業系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步程式碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長。

改進方式

JDK 6中為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”。共有4種鎖的狀態

鎖狀態儲存內容儲存內容
無鎖物件的hashCode、物件分代年齡、是否是偏向鎖(0)01
偏向鎖偏向執行緒ID、偏向時間戳、物件分代年齡、是否是偏向鎖(1)01
輕量級鎖指向棧中鎖記錄的指標00
重量級鎖指向互斥量(重量級鎖)的指標10

Java中的鎖

鎖的總結圖

在這裡插入圖片描述

樂觀鎖和悲觀鎖

樂觀鎖

樂觀鎖認為自己在使用資料時不會有別的執行緒修改資料,所以不會新增鎖,只是在更新資料的時候去判斷之前有沒有別的執行緒更新了這個資料。如果這個資料沒有被更新,當前執行緒將自己修改的資料成功寫入。如果資料已經被其他執行緒更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)。

例子

在這裡插入圖片描述

悲觀鎖

對於同一個資料的併發操作,悲觀鎖認為自己在使用資料的時候一定有別的執行緒來修改資料,因此在獲取資料的時候會先加鎖,確保資料不會被別的執行緒修改。Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。

例子

在這裡插入圖片描述

適用場景

  • 樂觀鎖適合讀操作多的場景,不加鎖可以大幅提高讀操作的效率。
  • 悲觀鎖適合寫操作多的場景,先加鎖,保證運算元據的正確性。

非公平鎖和公平鎖

公平鎖

公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,執行緒直接進入佇列中排隊,佇列中的第一個執行緒才能獲得鎖,其他執行緒按申請鎖的順序排隊。

優點

等待鎖的執行緒不會餓死。

缺點

整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大。

例子

在這裡插入圖片描述

如上圖所示,假設學生排隊打飯,飯堂阿姨有一把鎖,只有拿到鎖的人才能夠打飯,打完飯要把鎖還給飯堂阿姨。每個過來打飯的人都要飯堂阿姨的允許並拿到鎖之後才能去打飯,如果前面有人正在打飯,那麼這個想要打飯的學生就必須排隊。飯堂阿姨會檢視下一個要去打飯的人是不是隊伍裡排最前面的人,如果是的話,才會給你鎖讓你去打飯;如果你不是排第一的人,就必須去隊尾排隊,這就是公平鎖。

原始碼

ReentrantLock類裡面的tryAcquire方法

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

非公平鎖

非公平鎖是多個執行緒加鎖時直接嘗試獲取鎖,獲取不到才會到等待佇列的隊尾等待。但如果此時鎖剛好可用,那麼這個執行緒可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的執行緒先獲取鎖的場景。

優點

是可以減少喚起執行緒的開銷,整體的吞吐效率高,因為執行緒有機率不阻塞直接獲得鎖,CPU不必喚醒所有執行緒。

缺點

處於等待佇列中的執行緒可能會餓死,或者等很久才會獲得鎖。

例子

在這裡插入圖片描述

對於非公平鎖,就相當於來了一個壞學生,壞學生想要插隊,如果在上一個人剛打完飯把鎖還給飯堂阿姨而且飯堂阿姨還沒有允許等待隊伍裡下一個人去打飯時,剛好來了一個插隊的人,這個插隊的人是可以直接從飯堂阿姨那裡拿到鎖去打飯,不需要排隊,原本排隊等待的人只能繼續等待。

原始碼

ReentrantLock類裡面的nonfairTryAcquire方法

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

兩者原始碼區別

公平鎖與非公平鎖的lock()方法唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()。hasQueuedPredecessors()原始碼如下

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

該方法主要做一件事情:就是判斷當前執行緒是否位於同步佇列中的第一個。如果是則返回true,否則返回false。

獨享鎖和共享鎖

ReentrantReadWriteLock的部分原始碼,可以看到裡面有ReadLock和WriteLock這兩把鎖,ReadLock是共享鎖,WriteLock是獨享鎖。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;
}

獨享鎖

獨享鎖也叫排他鎖,是指該鎖一次只能被一個執行緒所持有。如果執行緒T對資料A加上排它鎖後,則其他執行緒不能再對A加任何型別的鎖。獲得排它鎖的執行緒即能讀資料又能修改資料。JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。

原始碼

public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;

   
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

共享鎖

共享鎖是指該鎖可被多個執行緒所持有。如果執行緒T對資料A加上共享鎖後,則其他執行緒只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的執行緒只能讀資料,不能修改資料。

原始碼

public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;

        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
}

讀鎖和寫鎖的加鎖方式區別

首先利用state欄位儲存讀鎖和寫鎖。在獨享鎖中這個值通常是0或者1(如果是重入鎖的話state值就是重入的次數),在共享鎖中state就是持有鎖的數量。由於在ReentrantReadWriteLock中有讀、寫兩把鎖,所以需要在一個整型變數state(int 32位)上分別描述讀鎖和寫鎖的數量。於是將state變數按位切割切分成了兩個部分,高16位表示讀鎖的狀態(讀鎖個數),低16位表示寫鎖的狀態(寫鎖個數)。

在這裡插入圖片描述

讀鎖原始碼

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

執行步驟

  1. 如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。
  2. 如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(執行緒安全的,可能有多個讀執行緒同時釋放讀鎖)均減少讀狀態,減少的值是“1<<16”。

寫鎖原始碼

static final int SHARED_SHIFT   = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();//獲取鎖的個數
    int w = exclusiveCount(c);//把寫鎖的數目
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())//若寫鎖數量為0或持有鎖的執行緒不是當前執行緒就返回失敗。
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)//如果寫入鎖的數量大於最大數(65535,2的16次方-1)就丟擲一個Error。
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))//如果c=0,即寫執行緒數為0,鎖的數量為0,若當前執行緒需要阻塞那麼就返回失敗或通過CAS增加寫執行緒數失敗也返回失敗。
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

執行步驟

  1. 由c = getState()獲取鎖的個數,然後再通過c來獲取寫鎖的個數w。因為寫鎖是低16位,所以取低16位的最大值與當前的c做與運算**(c & EXCLUSIVE_MASK)**,高16位和0與運算後是0,剩下的就是低位運算的值,同時也是持有寫鎖的執行緒數目。
  2. 若寫鎖數量為0或持有鎖的執行緒不是當前執行緒就返回失敗。
  3. 如果寫入鎖的數量大於最大數(65535,2的16次方-1)就丟擲一個Error。
  4. 如果c=0,即寫執行緒數為0,鎖的數量為0,若當前執行緒需要阻塞那麼就返回失敗或通過CAS增加寫執行緒數失敗也返回失敗。
  5. 如果c=0,w=0或者c>0,w>0(重入),則設定當前執行緒或鎖的擁有者,返回成功。

自旋鎖和適應性自旋鎖

原理

自旋鎖的實現原理同樣是CAS,AtomicInteger中呼叫unsafe進行自增操作的原始碼中的do-while迴圈就是一個自旋操作,如果修改數值失敗則通過迴圈來執行自旋,直至修改成功。

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//若var2與var5不相等,則一直自旋。若相等更新它的值為var5 + var4

        return var5;//返回原來的值
    }

自旋鎖

自旋鎖是為了減少cpu來回切換和恢復現場的消耗,通過讓當前執行緒自旋,直到鎖定同步資源的執行緒釋放鎖,那麼當前執行緒就不用阻塞,而直接可以獲取同步資源。自旋等待雖然避免了執行緒切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的執行緒只會白浪費處理器資源。所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(預設是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起執行緒。

適應性自旋鎖

適應性自旋鎖的自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也是很有可能再次成1功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞執行緒,避免浪費處理器資源。

存在的問題

  1. ABA問題:CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變數前面新增版本號,每次變數更新的時候都把版本號加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
  2. 迴圈時間長開銷大:CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
  3. 只能保證一個共享變數的原子操作:對一個共享變數執行操作時,CAS能夠保證原子操作,但是對多個共享變數操作時,CAS是無法保證操作的原子性的。解決方法是用AtomicReference類來保證引用物件之間的原子性,可以把多個變數放在一個物件裡來進行CAS操作。

例子

在這裡插入圖片描述

無鎖、偏向鎖、輕量級鎖、重量級鎖

無鎖

無鎖不對資源進行鎖定,所有的執行緒都能訪問並修改同一個資源,但同時只有一個執行緒能修改成功。

優點

  1. 無鎖的特點就是修改操作在迴圈內進行,執行緒會不斷的嘗試修改共享資源。
  2. 如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。
  3. 如果有多個執行緒修改同一個值,必定會有一個執行緒能修改成功,而其他修改失敗的執行緒會不斷重試直到修改成功。(CAS原理及應用即是無鎖的實現)

偏向鎖

偏向鎖是指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖,降低獲取鎖的代價。

優點

  1. 在大多數情況下,鎖總是由同一執行緒多次獲得,不存在多執行緒競爭,所以出現了偏向鎖。其目標就是在只有一個執行緒執行同步程式碼塊時能夠提高效能。
  2. 因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,偏向鎖可以避免在無多執行緒競爭的情況下不必要的輕量級鎖執行路徑。
  3. 避免自旋操作,消耗cpu。

原理

當一個執行緒訪問同步程式碼塊並獲取鎖時,會在Mark Word裡儲存鎖偏向的執行緒ID。線上程進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word裡是否儲存著指向當前執行緒的偏向鎖。引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令即可。

偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。

使用方法

在JDK 6及以後的JVM裡是預設啟用的。可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程式預設會進入輕量級鎖狀態。

輕量級鎖

當鎖是偏向鎖的時候,被另外的執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高效能。

優點

  1. 通過自旋,減少cpu切換的消耗。
  2. 避免執行緒阻塞和喚醒而影響效能

原理

在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,然後拷貝物件頭中的Mark Word複製到鎖記錄中。拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock Record裡的owner指標指向物件的Mark Word。如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,表示此物件處於輕量級鎖定狀態。

如果輕量級鎖的更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明多個執行緒競爭鎖。

若當前只有一個等待執行緒,則該執行緒通過自旋進行等待。但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。

重量級鎖

變為重量級鎖時,鎖標誌的狀態值變為“10”,此時Mark Word中儲存的是指向重量級鎖的指標,此時等待鎖的執行緒都會進入阻塞狀態。

鎖轉換流程(synchronized鎖狀態轉換)

無鎖---->偏向鎖---->輕量級鎖---->重量級鎖

在這裡插入圖片描述

偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免執行緒阻塞和喚醒而影響效能。重量級鎖是將除了擁有鎖的執行緒以外的執行緒都阻塞。

可重入鎖和非可重入鎖

可重入鎖

是指在同一個執行緒在外層方法獲取鎖的時候,再進入該執行緒的內層方法會自動獲取鎖(前提鎖物件得是同一個物件或者class),不會因為之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖。

public class Demo {
    public synchronized void A() {
        System.out.println("方法A執行...");
        B();
    }

    public synchronized void B() {
        System.out.println("方法B執行...");
    }
}

上面程式碼中,類中的兩個方法都是被內建鎖synchronized修飾的,A()方法中呼叫B()方法。因為內建鎖是可重入的,所以同一個執行緒在呼叫B()時可以直接獲得當前物件的鎖,進入B()進行操作。

優點

可一定程度避免死鎖

例子

在這裡插入圖片描述

有多個學生在排隊打飯,此時飯堂阿姨允許一個人打多份飯,第一個飯盒與鎖繫結後開始打飯,第二個飯盒也可以直接與鎖繫結打飯,第一個人所有飯盒打完飯後,才把鎖還給飯堂阿姨。這個人的所有打飯流程都能夠成功執行,後續等待的人也能夠打到飯。這就是可重入鎖。

非可重入鎖

當前執行緒在呼叫B之前需要將執行A()時獲取當前物件的鎖釋放掉,實際上該物件鎖已被當前執行緒所持有,且無法釋放。所以此時會出現死鎖。

例子

在這裡插入圖片描述

此時飯堂阿姨只允許鎖和同一個人的一個飯盒繫結打飯。第一個飯盒和鎖繫結打完飯之後並不會釋放鎖,導致第二個飯盒不能和鎖繫結也無法打飯。當前執行緒出現死鎖,整個等待佇列中的所有執行緒都無法被喚醒。

CAS的含義 ABA問題

CAS是讓CPU比較記憶體中某個值是否和預期的值相同,如果相同則將這個值更新為新值,不相同則不做更新,也就是CAS是原子性的操作(讀和寫兩者同時具有原子性),其實現方式是通過藉助C/C++呼叫CPU指令完成的,所以效率很高。

public boolean compareAndSwap(int value, int expect, int update) {
//        如果記憶體中的值value和期望值expect一樣 則將值更新為新值update
    if (value == expect) {
        value = update;
        return true;
    } else {
        return false;
    }
}

ABA問題

在多執行緒場景下CAS會出現ABA問題,例如有2個執行緒同時對同一個值(初始值為A)進行CAS操作,這三個執行緒如下

  1. 執行緒1,期望值為A,欲更新的值為B
  2. 執行緒2,期望值為A,欲更新的值為B

執行緒1搶先獲得CPU時間片,而執行緒2因為其他原因阻塞了,執行緒1取值與期望的A值比較,發現相等然後將值更新為B,然後這個時候出現了執行緒3,期望值為B,欲更新的值為A,執行緒3取值與期望的值B比較,發現相等則將值更新為A,此時執行緒2從阻塞中恢復,並且獲得了CPU時間片,這時候執行緒2取值與期望的值A比較,發現相等則將值更新為B,雖然執行緒2也完成了操作,但是執行緒2並不知道值已經經過了A->B->A的變化過程。

解決方法

在變數前面加上版本號,每次變數更新的時候變數的版本號都+1,即A->B->A就變成了1A->2B->3A

volatile關鍵字

volatile關鍵字是一個輕量級的鎖,可以保證可見性和有序性,但是不保證原子性。

volatile特點

  1. volatile 可以保證主記憶體和工作記憶體直接產生互動,進行讀寫操作,保證可見性
  2. volatile 僅能保證變數寫操作的原子性,不能保證讀寫操作的原子性。
  3. volatile可以禁止指令重排序(通過插入記憶體屏障),典型案例是在單例模式中使用。

volatile變數的開銷

volatile不會導致執行緒上下文切換,但是其讀取變數的成本較高,因為其每次都需要從快取記憶體或者主記憶體中讀取,無法直接從暫存器中讀取變數。

volatile在什麼情況下可以替代鎖

olatile是一個輕量級的鎖,適合多個執行緒共享一個狀態變數,鎖適合多個執行緒共享一組狀態變數。可以將多個執行緒共享的一組狀態變數合併成一個物件,用一個volatile變數來引用該物件,從而替代鎖。

說一下volatile 指令不可重排 怎麼不可重排,為什麼不可重排,重排會發生什麼

舉例:當Instance instance = new Instance(),Jvm執行了以下操作

  1. 在堆記憶體上分配物件的記憶體空間
  2. 在堆記憶體上初始化物件
  3. 設定instance指向剛分配的記憶體地址

第二步和第三步可能會發生重排序,導致引用型變數指向了一個不為null但是也不完整的物件。(在多執行緒下的單例模式中,必須通過volatile來禁止指令重排序

synchronized三種使用方式

它可以使用在方法上和方法塊上,表示同步方法和同步程式碼塊

synchronized實現原理

synchronized是Java中的一個關鍵字,是一個內部鎖。它可以使用在方法上和方法塊上,表示同步方法和同步程式碼塊。在多執行緒環境下,同步方法或者同步程式碼塊在同一時刻只允許有一個執行緒在執行,其餘執行緒都在等待獲取鎖,也就是實現了整體併發中的區域性序列。

實現原理

  1. 進入時,執行monitorenter,將計數器+1,釋放鎖monitorexit時,計數器-1
  2. 當一個執行緒判斷到計數器為0時,則當前鎖空閒,可以佔用;反之,當前執行緒進入等待狀態

synchronized和ReentrantLock的區別

ReentrantLock是顯示鎖,其提供了一些內部鎖不具備的特性,但並不是內部鎖的替代品。顯式鎖支援公平和非公平的排程方式,預設採用非公平排程。

synchronized 內部鎖簡單,但是不靈活。顯示鎖支援在一個方法內申請鎖,並且在另一個方法裡釋放鎖。顯示鎖定義了一個tryLock()方法,嘗試去獲取鎖,成功返回true,失敗並不會導致其執行的執行緒被暫停而是直接返回false,即可以避免死鎖

AQS(AbstractQueuedSynchronizer)的原理與實現。

後續完善

CountDownLatch、CyclicBarrier介紹

CountDownLatch

CountDownLatch是一個倒數計時協調器,它可以實現一個或者多個執行緒等待其餘執行緒完成一組特定的操作之後,繼續執行。

內部實現原理

  1. CountDownLatch內部維護一個計數器,CountDownLatch.countDown()每被執行一次都會使計數器值減少1。
  2. 當計數器不為0時,CountDownLatch.await()方法的呼叫將會導致執行執行緒被暫停,這些執行緒就叫做該CountDownLatch上的等待執行緒。
  3. CountDownLatch.countDown()相當於一個通知方法,當計數器值達到0時,喚醒所有等待執行緒。當然對應還有指定等待時間長度的CountDownLatch.await( long , TimeUnit)方法。

CyclicBarrier

CyclicBarrier一個柵欄,可以實現多個執行緒相互等待執行到指定的地點,這時候這些執行緒會再接著執行,在實際工作中可以用來模擬高併發請求測試

內部實現原理

  1. 使用CyclicBarrier實現等待的執行緒被稱為參與方(Party),參與方只需要執行CyclicBarrier.await()就可以實現等待,該柵欄維護了一個顯示鎖,可以識別出最後一個參與方,當最後一個參與方呼叫await()方法時,前面等待的參與方都會被喚醒,並且該最後一個參與方也不會被暫停。
  2. CyclicBarrier內部維護了一個計數器變數count = 參與方的個數,呼叫await方法可以使得count -1。當判斷到是最後一個參與方時,呼叫singalAll喚醒所有執行緒。

JUC原子類

介紹Atomic之前先來看一個問題吧,i++操作是執行緒安全的嗎

i++操作並不是執行緒安全的,它是一個複合操作,包含三個步驟:

  • 拷貝i的值到臨時變數
  • 臨時變數++操作
  • 拷貝回原始變數i

這是一個複合操作,不能保證原子性,所以這不是執行緒安全的操作。那麼如何實現原子自增等操作呢?

這裡就用到了JDK在java.util.concurrent.atomic包下的AtomicInteger等原子類了。AtomicInteger類提供了getAndIncrement和incrementAndGet等原子性的自增自減等操作Atomic等原子類內部使用了CAS來保證原子性。

兩個執行緒讀,一個執行緒寫,如何實現,不用鎖可以嗎

可以,使用volatile

執行緒池作用、引數、有幾種

作用

  1. 執行緒池可以重複利用已建立的執行緒,一次建立可以執行多次任務,有效降低執行緒建立和銷燬所造成的資源消耗;
  2. 執行緒池技術使得請求可以快速得到響應,節約了建立執行緒的時間;
  3. 執行緒的建立需要佔用系統記憶體,消耗系統資源,使用執行緒池可以更好的管理執行緒,做到統一分配、調優和監控執行緒,提高系統的穩定性。

引數

 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:拒絕策略

常見的執行緒池型別

  1. newCachedThreadPool( )
    1. 核心執行緒池大小為0,最大執行緒池大小不受限,來一個建立一個執行緒
    2. 適合用來執行大量耗時較短且提交頻率較高的任務
  2. newFixedThreadPool( )
    1. 固定大小的執行緒池
    2. 當執行緒池大小達到核心執行緒池大小,就不會增加也不會減小工作者執行緒的固定大小的執行緒池
  3. newSingleThreadExecutor( )
    1. 便於實現單(多)生產者-消費者模式

workQueue阻塞佇列

  1. ArrayBlockingQueue
    1. 內部使用一個陣列作為其儲存空間,陣列的儲存空間是預先分配
    2. 優點是 put 和 take操作不會增加GC的負擔(因為空間是預先分配的)
    3. 缺點是 put 和 take操作使用同一個鎖,可能導致鎖爭用,導致較多的上下文切換。
    4. ArrayBlockingQueue適合在生產者執行緒和消費者執行緒之間的併發程式較低的情況下使用。
  2. LinkedBlockingQueue
    1. 是一個無界佇列(其實佇列長度是Integer.MAX_VALUE)
    2. 內部儲存空間是一個連結串列,並且連結串列節點所需的儲存空間是動態分配
    3. 優點是 put 和 take 操作使用兩個顯式鎖(putLock和takeLock)
    4. 缺點是增加了GC的負擔,因為空間是動態分配的。
    5. LinkedBlockingQueue適合在生產者執行緒和消費者執行緒之間的併發程式較高的情況下使用。
  3. SynchronousQueue
    1. SynchronousQueue可以被看做一種特殊的有界佇列。生產者執行緒生產一個產品之後,會等待消費者執行緒來取走這個產品,才會接著生產下一個產品,適合在生產者執行緒和消費者執行緒之間的處理能力相差不大的情況下使用。

拒絕策略有幾種

  1. 如果執行的執行緒少於corePoolSize,則Executor始終首選新增新的執行緒,而不進行排隊
  2. 如果執行的執行緒等於或者多於corePoolSize,則Executor始終首選將請求加入佇列,而不是新增新執行緒
  3. 如果無法將請求加入佇列,即佇列已經滿了,則建立新的執行緒,除非建立此執行緒超出maxinumPoolSize,在這種情況下,任務預設將被拒絕。

Java中如何正常終止執行緒

Stop方法

stop方法已經被廢棄。原因是stop()方法會強行把執行一半的執行緒終止。這樣會就不會保證執行緒的資源正確釋放,通常是沒有給與執行緒完成資源釋放工作的機會,因此會導致程式工作在不確定的狀態下。

例子:

public class Demo {

    public static User user = new User();

    // 改變user變數的執行緒
    public static class ChangeObjectThread extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (Demo.class) {
                    int v = (int) (System.currentTimeMillis() / 1000);
                    user.setId(v);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    user.setName(String.valueOf(v));
                }
                // 讓出CPU,給其他執行緒執行
                Thread.yield();
            }

        }

    }
    // 讀取user變數的執行緒
    public static class ReadObjectThread extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (Demo.class) {
                    if (user.getId() != Integer.parseInt(user.getName())) {
                        System.out.println(user.toString());
                    }
                }
                // 讓出CPU,給其他執行緒執行
                Thread.yield();
            }

        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReadObjectThread().start();
        while (true) {
            Thread t = new ChangeObjectThread();
            t.start();
            Thread.sleep(150);
            //使用stop()方法,強制停止執行緒
            t.stop();
        }

    }
}

class User {
    private int id;
    private String name;

    public User() {
        this(0, "0");
    }

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

/**輸出
User{id=1601881231, name='1601881230'}
User{id=1601881231, name='1601881230'}
User{id=1601881232, name='1601881231'}
User{id=1601881233, name='1601881232'}
User{id=1601881233, name='1601881232'}
User{id=1601881233, name='1601881232'}

*/

當執行ReadObjectThread方法的時候,如果id和name不一樣就會輸出這個物件。ChangeObjectThread總是寫入連個相同的數值,但是在程式碼中因為使用了stop()強行停止執行緒,造成了資料的不同步。

利用boolean變數中止執行緒

如果需要停止一個執行緒時,只是需要執行時確定執行緒什麼時候退出就可以了。

class TheadDemo implements Runnable {
    boolean isBreak=false;

    public void setBreak() {
        isBreak = true;
    }
    @Override
    public void run() {
        while (!isBreak) {
            synchronized (Demo.class) {
                //TODO
            }
            Thread.yield();
        }
    }
}

interrupt方法

中斷可以理解為執行緒的一個標識位屬性,它表示一個執行中的執行緒是否被其他執行緒進行了中斷操作。其他執行緒通過呼叫該執行緒的interrupt()方法對其進行中斷操作。

執行緒通過方法isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位。如果該執行緒已經處於終結狀態,即使該執行緒被中斷過,在呼叫該執行緒物件的isInterrupted()時依舊會返回false。

public class Demo {
    public static void main(String[] args) throws Exception {
        // ThreadA睡眠10s
        Thread threadA = new Thread(new ThreadA(), "ThreadA");
        threadA.setDaemon(true);
        // ThreadB不停的執行
        Thread threadB = new Thread(new ThreadB(), "ThreadB");
        threadB.setDaemon(true);
        threadA.start();
        threadB.start();
        // 休眠5秒,讓sleepThread和busyThread充分執行
        TimeUnit.SECONDS.sleep(5);
        threadA.interrupt();
        threadB.interrupt();
        System.out.println("threadA interrupted is "
                + threadA.isInterrupted());
        System.out.println("threadB interrupted is "
                + threadB.isInterrupted());
        // 防止sleepThread和busyThread立刻退出
        TimeUnit.SECONDS.sleep(2);
    }

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (true) {
            }
        }
    }
}
/**輸出
ThreadA interrupted is false
ThreadB interrupted is true
/

由結果得知,在threadA執行時被中斷會許多宣告丟擲InterruptedException的方法這,Java虛擬機器會在這方法在丟擲InterruptedException之前先將該執行緒的中斷標識位清除,然後丟擲InterruptedException,此時呼叫isInterrupted()方法將會返回false。而threadB則被正常中斷,isInterrupted()方法返回true。

利用中止機制中斷執行緒

public class Demo {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    // 使用中斷機制,來終止執行緒
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("Interrupted");
                        break;
                    }
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        System.out.println(e.getMessage());
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(200);
        thread.interrupt();

    }

}
/**
sleep interrupted
Interrupted
/

主執行緒可以捕獲子執行緒丟擲的異常嗎

可以,程式碼如下

public class Main {

    public static void main(String[] args) {
        try {
            Thread thread = new Thread(new ThreadExceptionRunner());
            thread.start();
        } catch (Exception e) {
            System.out.println("-------");
            e.printStackTrace();
        }finally {
            
        }
        System.out.println("開始捕捉子執行緒的異常");
        ExecutorService executorService= Executors.newCachedThreadPool(new HandleThreadFactory());
        executorService.execute(new ThreadExceptionRunner());
        executorService.shutdown();

    }
}

class ThreadExceptionRunner implements Runnable {
    @Override
    public void run() {
        throw new RuntimeException("error !");
    }
}

class MyUncaughtExceptionHandle implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("Caught Error:" + e);

    }
}
class HandleThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        System.out.println("create thread t");
        Thread t = new Thread(r);
        System.out.println("set uncaughtException for t");
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
        return t;
    }
}

/** 輸出
Exception in thread "Thread-0" java.lang.RuntimeException: error !
	at 測試.ThreadExceptionRunner.run(Main.java:31)
	at java.lang.Thread.run(Thread.java:748)
開始捕捉子執行緒的異常
create thread t
set uncaughtException for t
Caught Error:java.lang.RuntimeException: error !
/

相關文章