1. 程式人生 > 其它 >深入瞭解jvm-2Edition-執行緒安全與鎖優化

深入瞭解jvm-2Edition-執行緒安全與鎖優化

1、什麼是執行緒安全

  Brian Goetz 在 《Java Concurrency In Practice》中定義的執行緒安全為:

  “當多執行緒訪問一個物件時,無論這些執行緒在執行時環境下以何種方式排程和交替執行,

  在使用物件時,都不需要任何額外的同步,就可以得到正確的行為和結果,那麼這個物件就是執行緒安全的。”

  Java中的執行緒安全可分為:

    1、不可變

      物件被安全構造(安全釋出)後狀態永不改變,那麼永遠是執行緒安全的。

    2、絕對執行緒安全

      滿足Brian Goetz 的執行緒安全的定義。

    3、相對執行緒安全

      只保證對物件單獨的操作是執行緒安全的,複合操作還是要進行同步。

    4、執行緒相容

      物件本身不是執行緒安全的,但是可以通過呼叫端使用同步來保證安全使用。

    5、執行緒對立

      無論是否採取同步,都無法安全併發使用的程式碼。

      如:Thread.suspend() 和 Thread.resume() 有死鎖風險。

2、執行緒安全實現方法

  1、互斥同步-阻塞同步

    通過使用互斥來保證同步。

    臨界區、互斥量、訊號量。

    synchronized同步程式碼塊:

      可重入、阻塞或喚醒需要作業系統完成。非公平,只能繫結一個喚醒條件。

    ReentrantLock:

      可重入,等待可中斷、可實現公平、可繫結多個喚醒條件。

    因為jdk對synchronized做了優化,能用synchronized實現需求時儘量使用synchronized。

  2、非阻塞同步

    阻塞同步是一種悲觀的併發策略,認為只要沒有正確的同步措施,就一定會出現問題。

    非阻塞同步則是樂觀的,基於衝突檢測,沒有衝突就算成功,有衝突就解決衝突(重新試)。

    樂觀併發策略需要硬體幫助,因為要保證操作和檢測衝突是原子的。

    常用的原子指令有:

      Test-and-Set;

      Fetch-and-Increment;

      Swap;

      Compare-and-Swap;

      Load-Linked / Store-Conditional。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrencyWithAtomic {
    private static final int THREADS_COUNT=20;
    public static AtomicInteger race=new AtomicInteger(0);
    public static void increase(){
        race.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService= Executors.newCachedThreadPool();
        for(int i=0; i<THREADS_COUNT; i++) {
            executorService.execute(()->{
                for(int j=0; j<THREADS_COUNT; j++)
                    increase();
            });
        }
        while(!executorService.awaitTermination(1000, TimeUnit.MILLISECONDS))
        executorService.shutdown();
        System.out.println(race);
    }
}

    其中incrementAndGet的原始碼為:

/**
     * Atomically increments the current value,
     * with memory effects as specified by {@link VarHandle#getAndAdd}.
     *
     * <p>Equivalent to {@code addAndGet(1)}.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

/**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object {@code o}
     * at the given {@code offset}.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    @IntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

    可以看到,getAndAddInt(Object o, long offset, int delta) 方法是實現了棧封閉的。

  3、無同步方案

    可重入程式碼,棧封閉;

    ThreadLocal實現執行緒封閉。

3、鎖優化

  1、旋轉鎖和自適應鎖

    由於在掛起和恢復執行緒時,需要陷入核心,因此開銷較大。

    因此,選擇在鎖持續時間短時,採用忙等待的形式。

    自適應鎖就是虛擬機器會自動設定忙等待的迴圈次數。

  2、鎖消除

    由逃逸分析資料支援,在實現了棧封閉時,就不需要鎖了。

    這時候就可以將鎖消除掉。

  3、鎖粗化

    我們寫程式碼時,為了減少同步操作的數量,總是習慣將同步程式碼塊縮小。

    但是,如果同步程式碼塊小而密集的時候,頻繁的進行互斥操作會導致效能損耗。

    這是,虛擬機器會將同步程式碼塊的範圍擴大。

4、輕量級鎖

  在沒有多執行緒競爭的時候,減少互斥產生的效能消耗。

  HotSpot虛擬機器物件頭結構:

    物件頭分為兩部分,一部分用於存貯物件自身的執行時資料(HashCode、GC Age)。

    另一部分用於儲存指向方法區中對應的元資料的引用。

    第一部分也被稱為Mark Word,HotSpot虛擬機器Mark Word採用了非固定的資料結構。

    在每種結構下,都有一個標誌位。

    結構和標誌位一起確定了物件的狀態。

  

  

  線上程進入同步程式碼塊時,如果此時物件沒有被鎖定,

  虛擬機器將首先線上程的棧幀中建立一個鎖記錄空間(Lock Record),用於儲存物件的當前Mark Word的拷貝。

  然後,虛擬機器使用CAS操作將物件Mark Word的值更新為指向鎖記錄空間的指標。

  如果更新成功,那麼該物件就被輕量級鎖定了,Mark Word的標誌位變為00。

  如果更新失敗,檢查物件的Mark Word是否指向當前執行緒,是,則可進入同步程式碼塊。

  否,則說明物件已經被其他執行緒鎖定了。

  此時,出現了兩個以上的執行緒爭用同一個鎖,輕量級鎖不再起效,要膨脹為重量級鎖。

  鎖標記的標記位要變為 10。

  釋放鎖的時候,也要通過CAS操作來完成。如果替換失敗,要釋放鎖的同時,喚醒被掛起的執行緒。

  偏向鎖:

    偏向鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖沒有被其他執行緒獲取,

    則持有鎖的執行緒不需要再進行同步。

    一旦有執行緒嘗試獲取這個鎖,偏向模式就結束。