1. 程式人生 > 實用技巧 >JAVA高併發程式設計

JAVA高併發程式設計

synchronized 關鍵字

可用來給物件和方法或者程式碼塊加鎖,當它鎖定一個方法或者一個程式碼塊的時候,同一時刻最多隻有一個執行緒執行這段程式碼。可能鎖物件包括: this, 臨界資源物件,Class 類物件

同步方法

同步方法鎖定的是當前物件。當多執行緒通過同一個物件引用多次呼叫當前同步方法時, 需同步執行。

public synchronized void test(){
        System.out.println("測試一下");
    }
  • 1
  • 2
  • 3

同步程式碼塊

同步程式碼塊的同步粒度更加細緻,是商業開發中推薦的程式設計方式。可以定位到具體的同步位置,而不是簡單的將方法整體實現同步邏輯。在效率上,相對更高。
鎖定臨界物件
同步程式碼塊在執行時,是鎖定 object 物件。當多個執行緒呼叫同一個方法時,鎖定物件不變的情況下,需同步執行。

public void test(){
        synchronized(o){
            System.out.println("測試一下");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5

鎖定當前物件

public void test(){
        synchronized(this){
            System.out.println("測試一下");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5

鎖的底層實現

Java 虛擬機器中的同步(Synchronization)基於進入和退出管程(Monitor)物件實現。同步方法 並不是由 monitor enter 和 monitor exit 指令來實現同步的,而是由方法呼叫指令讀取執行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的。

物件頭:儲存物件的 hashCode、鎖資訊或分代年齡或 GC 標誌,型別指標指向物件的類元資料,JVM 通過這個指標確定該物件是哪個類的例項等資訊。
例項變數:存放類的屬性資料資訊,包括父類的屬性資訊
填充資料:由於虛擬機器要求物件起始地址必須是 8 位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊
當在物件上加鎖時,資料是記錄在物件頭中。當執行 synchronized 同步方法或同步程式碼塊時,會在物件頭中記錄鎖標記,鎖標記指向的是 monitor 物件(也稱為管程或監視器鎖) 的起始地址。每個物件都存在著一個 monitor 與之關聯,物件與其 monitor 之間的關係有存在多種實現方式,如 monitor 可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個 monitor 被某個執行緒持有後,它便處於鎖定狀態。


在 Java 虛擬機器(HotSpot)中,monitor 是由 ObjectMonitor 實現的。
ObjectMonitor 中有兩個佇列,_WaitSet 和 _EntryList,以及_Owner 標記。其中_WaitSet 是用於管理等待佇列(wait)執行緒的,_EntryList 是用於管理鎖池阻塞執行緒的,_Owner 標記用於記錄當前執行執行緒。執行緒狀態圖如下:

當多執行緒併發訪問同一個同步程式碼時,首先會進入_EntryList,當執行緒獲取鎖標記後,
monitor 中的_Owner 記錄此執行緒,並在 monitor 中的計數器執行遞增計算(+1),代表鎖定,其他執行緒在_EntryList 中繼續阻塞。若執行執行緒呼叫 wait 方法,則 monitor 中的計數器執行賦值為 0 計算,並將_Owner 標記賦值為 null,代表放棄鎖,執行執行緒進如_WaitSet 中阻塞。若執行執行緒呼叫 notify/notifyAll 方法,_WaitSet 中的執行緒被喚醒,進入_EntryList 中阻塞,等待獲取鎖標記。若執行執行緒的同步程式碼執行結束,同樣會釋放鎖標記,monitor 中的_Owner 標記賦值為 null,且計數器賦值為 0 計算。

鎖的種類

Java 中鎖的種類大致分為偏向鎖,自旋鎖,輕量級鎖,重量級鎖。
鎖的使用方式為:先提供偏向鎖,如果不滿足的時候,升級為輕量級鎖,再不滿足,升級為重量級鎖。自旋鎖是一個過渡的鎖狀態,不是一種實際的鎖型別。
鎖只能升級,不能降級。

重量級鎖

在鎖的底層實現中解釋的就是重量級鎖。

偏向鎖

是一種編譯解釋鎖。如果程式碼中不可能出現多執行緒併發爭搶同一個鎖的時候,JVM 編譯程式碼,解釋執行的時候,會自動的放棄同步資訊。消除 synchronized 的同步程式碼結果。使用鎖標記的形式記錄鎖狀態。在 Monitor 中有變數 ACC_SYNCHRONIZED。當變數值使用的時候, 代表偏向鎖鎖定。可以避免鎖的爭搶和鎖池狀態的維護。提高效率。

輕量級鎖
過渡鎖。當偏向鎖不滿足,也就是有多執行緒併發訪問,鎖定同一個物件的時候,先提升為輕量級鎖。也是使用標記 ACC_SYNCHRONIZED 標記記錄的。ACC_UNSYNCHRONIZED 標記記錄未獲取到鎖資訊的執行緒。就是隻有兩個執行緒爭搶鎖標記的時候,優先使用輕量級鎖。
兩個執行緒也可能出現重量級鎖。

自旋鎖
是一個過渡鎖,是偏向鎖和輕量級鎖的過渡。
當獲取鎖的過程中,未獲取到。為了提高效率,JVM 自動執行若干次空迴圈,再次申請鎖,而不是進入阻塞狀態的情況。稱為自旋鎖。自旋鎖提高效率就是避免執行緒狀態的變更。

volatile 關鍵字

變數的執行緒可見性。在 CPU 計算過程中,會將計算過程需要的資料載入到 CPU 計算快取中,當 CPU 計算中斷時,有可能重新整理快取,重新讀取記憶體中的資料。線上程執行的過程中,如果某變數被其他執行緒修改,可能造成資料不一致的情況,從而導致結果錯誤。而 volatile 修飾的變數是執行緒可見的,當 JVM 解釋 volatile 修飾的變數時,會通知 CPU,在計算過程中, 每次使用變數參與計算時,都會檢查記憶體中的資料是否發生變化,而不是一直使用 CPU 快取中的資料,可以保證計算結果的正確。
volatile 只是通知底層計算時,CPU 檢查記憶體資料,而不是讓一個變數在多個執行緒中同步。

volatile int count = 0;
  • 1

wait&notify

AtomicXxx 型別組

原子型別。
在 concurrent.atomic 包中定義了若干原子型別,這些型別中的每個方法都是保證了原子操作的。多執行緒併發訪問原子型別物件中的方法,不會出現資料錯誤。在多執行緒開發中,如果某資料需要多個執行緒同時操作,且要求計算原子性,可以考慮使用原子型別物件。

    AtomicInteger count = new AtomicInteger(0);
    void m(){
        count.incrementAndGet();
    }
  • 1
  • 2
  • 3
  • 4

注意:原子型別中的方法是保證了原子操作,但多個方法之間是沒有原子性的。如:

AtomicInteger i = new AtomicInteger(0); 
if(i.get() != 5){
    i.incrementAndGet();
}
  • 1
  • 2
  • 3
  • 4

在上述程式碼中,get 方法和 incrementAndGet 方法都是原子操作,但複合使用時,無法保證原子性,仍舊可能出現數據錯誤。

CountDownLatch 門閂

門閂是 concurrent 包中定義的一個型別,是用於多執行緒通訊的一個輔助型別。
門閂相當於在一個門上加多個鎖,當執行緒呼叫 await 方法時,會檢查門閂數量,如果門

閂數量大於 0,執行緒會阻塞等待。當執行緒呼叫 countDown 時,會遞減門閂的數量,當門閂數量為 0 時,await 阻塞執行緒可執行。

CountDownLatch latch = new CountDownLatch(5);

    void m1(){
        try {
            latch.await();// 等待門閂開放。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m1() method");
    }

    void m2(){
        for(int i = 0; i < 10; i++){
            if(latch.getCount() != 0){
                System.out.println("latch count : " + latch.getCount());
                latch.countDown(); // 減門閂上的鎖。
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("m2() method : " + i);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

鎖的重入

在 Java 中,同步鎖是可以重入的。只有同一執行緒呼叫同步方法或執行同步程式碼塊,對同一個物件加鎖時才可重入。
當執行緒持有鎖時,會在 monitor 的計數器中執行遞增計算,若當前執行緒呼叫其他同步程式碼,且同步程式碼的鎖物件相同時,monitor 中的計數器繼續遞增。每個同步程式碼執行結束,
monitor 中的計數器都會遞減,直至所有同步程式碼執行結束,monitor 中的計數器為 0 時,釋放鎖標記,_Owner 標記賦值為 null。

ReentrantLock

重入鎖,建議應用的同步方式。相對效率比 synchronized 高。量級較輕。
synchronized 在 JDK1.5 版本開始,嘗試優化。到 JDK1.7 版本後,優化效率已經非常好了。在絕對效率上,不比 reentrantLock 差多少。
使用重入鎖,必須必須必須手工釋放鎖標記。一般都是在 finally 程式碼塊中定義釋放鎖標記的 unlock 方法。

公平鎖

private static ReentrantLock lock = new ReentrantLock(true);
    public void run(){
        for(int i = 0; i < 5; i++){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName() + " get lock");
            }finally{
                lock.unlock();
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

8ThreadLocal

remove 問題

同步容器

解決併發情況下的容器執行緒安全問題的。給多執行緒環境準備一個執行緒安全的容器物件。執行緒安全的容器物件: Vector, Hashtable。執行緒安全容器物件,都是使用 synchronized
方法實現的。
concurrent 包中的同步容器,大多數是使用系統底層技術實現的執行緒安全。類似 native。
Java8 中使用 CAS。

Map/Set

ConcurrentHashMap/ConcurrentHashSet

底層雜湊實現的同步 Map(Set)。效率高,執行緒安全。使用系統底層技術實現執行緒安全。量級較 synchronized 低。key 和 value 不能為 null。

ConcurrentSkipListMap/ConcurrentSkipListSet

底層跳錶(SkipList)實現的同步 Map(Set)。有序,效率比 ConcurrentHashMap 稍低。

List

CopyOnWriteArrayList

寫時複製集合。寫入效率低,讀取效率高。每次寫入資料,都會建立一個新的底層陣列。

Queue

ConcurrentLinkedQueue

基礎連結串列同步佇列。

LinkedBlockingQueue
阻塞佇列,佇列容量不足自動阻塞,佇列容量為 0 自動阻塞。

ArrayBlockingQueue
底層陣列實現的有界佇列。自動阻塞。根據呼叫 API(add/put/offer)不同,有不同特性。
當容量不足的時候,有阻塞能力。
add 方法在容量不足的時候,丟擲異常。
put 方法在容量不足的時候,阻塞等待。
offer 方法,
單引數 offer 方法,不阻塞。容量不足的時候,返回 false。當前新增資料操作放棄。三引數 offer 方法(offer(value,times,timeunit)),容量不足的時候,阻塞 times 時長(單
位為 timeunit),如果在阻塞時長內,有容量空閒,新增資料返回 true。如果阻塞時長範圍
內,無容量空閒,放棄新增資料,返回 false。

DelayQueue
延時佇列。根據比較機制,實現自定義處理順序的佇列。常用於定時任務。如:定時關機。

LinkedTransferQueue
轉移佇列,使用 transfer 方法,實現資料的即時處理。沒有消費者,就阻塞。

SynchronusQueue
同步佇列,是一個容量為 0 的佇列。是一個特殊的 TransferQueue。必須現有消費執行緒等待,才能使用的佇列。
add 方法,無阻塞。若沒有消費執行緒阻塞等待資料,則丟擲異常。
put 方法,有阻塞。若沒有消費執行緒阻塞等待資料,則阻塞。

ThreadPool&Executor

Executor

執行緒池頂級介面。
常用方法 - void execute(Runnable)
作用是: 啟動執行緒任務的。

ExecutorService

Executor 介面的子介面。
常見方法 - Future submit(Callable), Future submit(Runnable)

Future

未來結果,代表執行緒任務執行結束後的結果。

Callable

可執行介面。
介面方法 : Object call();相當於 Runnable 介面中的 run 方法。區別為此方法有返回值。不能丟擲已檢查異常。
和 Runnable 介面的選擇 - 需要返回值或需要丟擲異常時,使用 Callable,其他情況可任意選擇。

Executors

工具型別。為 Executor 執行緒池提供工具方法。類似 Arrays,Collections 等工具型別的功用。

FixedThreadPool

容量固定的執行緒池
queued tasks - 任務佇列
completed tasks - 結束任務佇列

CachedThreadPool

快取的執行緒池。容量不限(Integer.MAX_VALUE)。自動擴容。預設執行緒空閒 60 秒,自動銷燬。

ScheduledThreadPool

計劃任務執行緒池。可以根據計劃自動執行任務的執行緒池。

SingleThreadExceutor

單一容量的執行緒池。

ForkJoinPool

分支合併執行緒池(mapduce 類似的設計思想)。適合用於處理複雜任務。初始化執行緒容量與 CPU 核心數相關。
執行緒池中執行的內容必須是 ForkJoinTask 的子型別(RecursiveTask,RecursiveAction)。

WorkStealingPool

JDK1.8 新增的執行緒池。工作竊取執行緒池。當執行緒池中有空閒連線時,自動到等待佇列中竊取未完成任務,自動執行。
初始化執行緒容量與 CPU 核心數相關。此執行緒池中維護的是精靈執行緒。
ExecutorService.newWorkStealingPool();

ThreadPoolExecutor

執行緒池底層實現。除 ForkJoinPool 外,其他常用執行緒池底層都是使用 ThreadPoolExecutor
實現的。
public ThreadPoolExecutor
(int corePoolSize, // 核心容量

int maximumPoolSize, // 最大容量
long keepAliveTime, // 生命週期,0 為永久
TimeUnit unit, // 生命週期單位
BlockingQueue workQueue // 任務佇列,阻塞佇列。