鎖優化:逃逸分析、自旋鎖、鎖消除、鎖粗化、輕量級鎖和偏向鎖


1. 逃逸分析 Escape Analysis
1.1 逃逸分為兩種:
  1. 方法逃逸:當一個對象在方法中被定義后,可能作為調用參數被外部方法說引用。
  2. 線程逃逸:通過復制給類變量或者作為實例變量在其他線程中可以被訪問到。
1.2 逃逸分析相關優化

如果證明一個對象不會逃逸方法外或者線程外,則可針對此變量進行一下三種優化:

  1. 棧上分配stack allocation:如果對象不會逃逸到方法外,則對此對象在棧上分配內存,則對象所占用的空間可以隨棧出棧而別銷毀。

  2. 同步消除synchronization Elimination:如果一個對象不會逃逸出線程,則對此變量的同步措施可消除。

  3. Scalar replacement:標量scalar是不可再分解的量,比如基本數據類型,聚合量Aggregate是可以在被分解的,比如java中的對象。標量替換是將一個聚合量拆散,根據程序對此聚合量的訪問情況,將其使用到的成員變量恢復到原始變量來訪問就是標量替換。逃逸分析如果證明一個對象不會被外部訪問,並且此對象可以被拆散,則程序執行時可能不會創建此對象

1.3 參數開啟
  • -XX:+DoEscapeAnalysis開啟逃逸分析;
  • -XX:+EliminateLocks開啟同步消除;
  • -XX:+EliminateAllocations開啟標量替換;
2.自旋鎖

線程等待的方式有兩種,掛起和自旋。

掛起操作需要進行線程調度,訪問jvm和操作系統共享的數據結構,而且線程初次啟動可能其所需數據不再處理器本地緩存中,由此也會引發一些開銷和緩存的缺失。

如果此線程想要獲取的鎖往往很快可以釋放,就讓其采取“本地自旋”操作。

如下是TASLock及其測試類示例代碼:

  • TASLock
/**
 * Tast And Set Loc:測試-設置鎖
 *
 * 自旋狀態時,每次嘗試獲取鎖,都采用了CAS(check and set)操作,
 * 不斷的設置鎖標志位—當鎖標志位可用時,一個線程拿到鎖,其他線程仍然自旋
 *
 * TODO 緩存一致性流量風暴
 *
 * fixme AtomicBoolean保存狀態,使用其getAndSet()方法判斷鎖狀態並嘗試獲取鎖
 */
public class TASLock implements Lock {
    //初始值為false;
    private AtomicBoolean mutex=new AtomicBoolean(false);


    @Override
    public void lock() {
        //返回之前的值,並設置為true fixme 如果之前未true則進入自旋狀態
        //fixme mutex之前狀態時FALSE時才返回,表示獲取到鎖
        //原子變量的改動對所有線程都可見
        while(mutex.getAndSet(true)){}
    }
    
    @Override
    public void unlock() {
        mutex.set(false);//fixme ?釋放鎖?
    }
}
  • 計時類
/**
 * 計時類
 */
public class TimeCost implements Lock {

    //實現時使用的是自旋鎖
    private final Lock lock;

    public TimeCost(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void lock() {
        long start=System.nanoTime();
        lock.lock();
        long duration=System.nanoTime()-start;

        System.out.println(lock.toString()+"time cost is "+duration);
    }

    @Override
    public void unlock() {
        lock.unlock();
    }
}

  • 驅動類
public class TASLockMain {
    private static TimeCost timeCost=new TimeCost(new TTASLock());
//  private static TimeCost timeCost=new TimeCost(new TASLock());

    public static void func(){
        timeCost.lock();
        timeCost.unlock();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            Thread t=new Thread(()-> func());
            t.start();
        }

    }
}
3.同步消除:鎖消除Lock Elision、鎖粗化Lock Coarsening

鎖消除:虛擬機的運行時編譯器在運行時如果檢測到一些要求同步的代碼上不可能發生共享數據競爭,則會去掉這些鎖。

鎖粗化:將臨近的代碼塊用同一個鎖合並起來。

參數設置:-XX:+EliminateLocks

消除無意義的鎖獲取和釋放,可以提高程序運行性能

4.輕量級鎖

輕量級鎖提升性能的依據是“絕大多數同步的鎖都是不存在競爭的”,對於競爭的情況輕量級鎖比傳統的重量級鎖更慢。

-XX:+UseHeavyMonitors可以禁用輕量級鎖和偏向鎖。

4.1 對象頭及其他

HotSpot虛擬機對象頭object header分為兩部分信息:

  1. mark word 對象自身的運行時信息,例如哈希碼hascode、GC分代年齡(generational GC age)。是實現輕量級鎖和偏向鎖的關鍵
  2. 存儲指向方法區中指向對象類型數據的指針;
  3. 如果是數組的話還有有額外的部分存放數組長度;

輕量級鎖相對於使用操作系統互斥量的互斥鎖(重量級鎖)synchronized、Lock而言的,使用CAS操作實現。

  • 對象頭中的mark word
4.2 同步:加鎖與解鎖

加鎖過程大致分為三步:

  1. 代碼進入同步塊時,檢測同步對象如果沒有被鎖定(01),則虛擬機首先在當前線程的棧幀中建立名為鎖記錄Lock Recode的空間(虛擬機棧為線程獨享,棧幀為其棧元素),並拷貝鎖對象對象頭的mark word部分。
  2. 虛擬機使用CAS操作嘗試將對象的mark word更新為指向棧幀的lock recode的指針,成功更新着此線程擁有了此鎖對象的鎖,並且對象的鎖標記更新為00,此對象處於輕量級鎖的狀態。如圖所示:
  3. 如歸CAS更新操作失敗了,則虛擬機檢查鎖對象的mark word是否指向當前線程的棧幀,是則說明線程已經擁有此對象鎖,進入同步代碼塊執行, 否則說明鎖對象被其他線程搶占,兩條以上的線程爭用一個鎖,輕量級鎖膨脹為重量級鎖,鎖對象狀態值變為10,鎖對象mark down存儲的是指向重量級鎖/互斥量/互斥鎖的指針,等待鎖的線程會進入阻塞狀態。

解鎖過程也是通過CAS操作

  1. 把把鎖對象的mark word和線程的棧幀中復制的mark word替換回來,成功者同步完成;
  2. 失敗說明有其他線程嘗試獲取該鎖,則在釋放鎖的同時喚醒掛起的線程。
5.偏向鎖 biased lock

偏向鎖更近一步,在無競爭的情況下直接把整個同步消除掉。如果程序大多數鎖總是被多個線程訪問則沒必要開啟。-XX:+UseBiasedLocking開啟偏向鎖。

5.1 獲取偏向鎖過程
  1. 鎖對象第一次被線程獲取時,使用CAS操作把獲取這個鎖的線程id記錄在mark word中,並更改標志位01,成功則此線程持有偏向鎖,持有偏向鎖的線程每次進入鎖對象相關的代碼塊時,虛擬機都不會進行任何同步操作
  2. 另一個線程嘗試獲取這個鎖時,偏向鎖撤銷。如果鎖對象未被鎖定則恢復到未被鎖定狀態,如果此對象正在被鎖定則變成輕量級鎖。
5.2 偏向鎖、輕量級鎖、重量級鎖/互斥鎖 狀態轉換圖


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM