二十九、“鎖”(十)JVM 對鎖的優(yōu)化

1、JVM 對鎖進行了哪些優(yōu)化?

相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虛擬機對 synchronized 內(nèi)置鎖的性能進行了很多優(yōu)化,包括自適應(yīng)的自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖等。有了這些優(yōu)化措施后,synchronized 鎖的性能得到了大幅提高。

自適應(yīng)的自旋鎖

首先,看一下自適應(yīng)的自旋鎖?!白孕本褪遣会尫?CPU,一直循環(huán)嘗試獲取鎖,如下面這段代碼:

public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
 
    return var6;
}

代碼中使用一個 do-while 循環(huán)來一直嘗試修改 long 的值。自旋的缺點在于如果自旋時間過長,那么性能開銷是很大的,浪費了 CPU 資源。

在 JDK 1.6 中引入了自適應(yīng)的自旋鎖來解決長時間自旋的問題。自適應(yīng)意味著自旋的時間不再固定,而是會根據(jù)最近自旋嘗試的成功率、失敗率,以及當(dāng)前鎖的擁有者的狀態(tài)等多種因素來共同決定。自旋的持續(xù)時間是變化的,自旋鎖變“聰明”了。比如,如果最近嘗試自旋獲取某一把鎖成功了,那么下一次可能還會繼續(xù)使用自旋,并且允許自旋更長的時間;但是如果最近自旋獲取某一把鎖失敗了,那么可能會省略掉自旋的過程,以便減少無用的自旋,提高效率。

鎖消除

第二個優(yōu)化是鎖消除。來看下面的代碼:

public class Person {

    private String name;
    private int age;

    public Person(String personName, int personAge) {
        name = personName;
        age = personAge;
    }

    public Person(Person p) {
        this(p.getName(), p.getAge());
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

class Employee {

    private Person person;

    // makes a defensive copy to protect against modifications by caller
    public Person getPerson() {
        return new Person(person);
    }


    public void printEmployeeDetail(Employee emp) {
        Person person = emp.getPerson();
        // this caller does not modify the object, so defensive copy was unnecessary
        System.out.println("Employee's name: " + person.getName() + "; age: " + person.getAge());
    }
}

在這段代碼中,下方的 Employee 類中的 getPerson() 方法,這個方法中使用了類里面的 person 對象,并且新建一個和它屬性完全相同的新的 person 對象,目的是防止方法調(diào)用者修改原來的 person 對象。但是在這個例子中,其實是沒有任何必要新建對象的,因為 printEmployeeDetail() 方法沒有對這個對象做出任何的修改,僅僅是打印,既然如此,可以直接打印最開始的 person 對象,而無須新建一個新的。
如果編譯器可以確定最開始的 person 對象不會被修改的話,它可能會優(yōu)化并且消除這個新建 person 的過程。

根據(jù)這樣的思想,接下來就來舉一個鎖消除的例子,經(jīng)過逃逸分析之后,如果發(fā)現(xiàn)某些對象不可能被其他線程訪問到,那么就可以把它們當(dāng)成棧上數(shù)據(jù),棧上數(shù)據(jù)由于只有本線程可以訪問,自然是線程安全的,也就無需加鎖,所以會把這樣的鎖給自動去除掉。
例如,StringBuffer 的 append 方法如下所示:

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

從代碼中可以看出,這個方法是被 synchronized 修飾的同步方法,因為它可能會被多個線程同時使用。
但是在大多數(shù)情況下,它只會在一個線程內(nèi)被使用,如果編譯器能確定這個 StringBuffer 對象只會在一個線程內(nèi)被使用,就代表肯定是線程安全的,那么編譯器便會做出優(yōu)化,把對應(yīng)的 synchronized 給消除,省去加鎖和解鎖的操作,以便增加整體的效率。

鎖粗化

接下來,介紹一下鎖粗化。如果釋放了鎖,緊接著什么都沒做,又重新獲取鎖,例如下面這段代碼所示:

public void lockCoarsening() {
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
}

那么其實這種釋放和重新獲取鎖是完全沒有必要的,如果把同步區(qū)域擴大,也就是只在最開始加一次鎖,并且在最后直接解鎖,那么就可以把中間這些無意義的解鎖和加鎖的過程消除,相當(dāng)于是把幾個 synchronized 塊合并為一個較大的同步塊。這樣做的好處在于在線程執(zhí)行這些代碼時,就無須頻繁申請與釋放鎖了,這樣就減少了性能開銷。
不過,這樣做也有一個副作用,那就是會讓同步區(qū)域變大。如果在循環(huán)中也這樣做,如代碼所示:

for (int i = 0; i < 1000; i++) {
    synchronized (this) {
        //do something
    }
}

也就是在第一次循環(huán)的開始,就開始擴大同步區(qū)域并持有鎖,直到最后一次循環(huán)結(jié)束,才結(jié)束同步代碼塊釋放鎖的話,這就會導(dǎo)致其他線程長時間無法獲得鎖。所以,這里的鎖粗化不適用于循環(huán)的場景,僅適用于非循環(huán)的場景。
鎖粗化功能是默認打開的,用 -XX:-EliminateLocks 可以關(guān)閉該功能。

偏向鎖/輕量級鎖/重量級鎖

下面來介紹一下偏向鎖、輕量級鎖和重量級鎖。這三種鎖是特指 synchronized 鎖的狀態(tài)的,通過在對象頭中的 mark word 來表明鎖的狀態(tài)。

  • 偏向鎖

對于偏向鎖而言,它的思想是如果自始至終,對于這把鎖都不存在競爭,那么其實就沒必要上鎖,只要打個標(biāo)記就行了。一個對象在被初始化后,如果還沒有任何線程來獲取它的鎖時,它就是可偏向的,當(dāng)有第一個線程來訪問它嘗試獲取鎖的時候,它就記錄下來這個線程,如果后面嘗試獲取鎖的線程正是這個偏向鎖的擁有者,就可以直接獲取鎖,開銷很小。

  • 輕量級鎖

JVM 的開發(fā)者發(fā)現(xiàn)在很多情況下,synchronized 中的代碼塊是被多個線程交替執(zhí)行的,也就是說,并不存在實際的競爭,或者是只有短時間的鎖競爭,用 CAS 就可以解決。這種情況下,重量級鎖是沒必要的。輕量級鎖指當(dāng)鎖原來是偏向鎖的時候,被另一個線程所訪問,說明存在競爭,那么偏向鎖就會升級為輕量級鎖,線程會通過自旋的方式嘗試獲取鎖,不會阻塞。

  • 重量級鎖

這種鎖利用操作系統(tǒng)的同步機制實現(xiàn),所以開銷比較大。當(dāng)多個線程直接有實際競爭,并且鎖競爭時間比較長的時候,此時偏向鎖和輕量級鎖都不能滿足需求,鎖就會膨脹為重量級鎖。重量級鎖會讓其他申請卻拿不到鎖的線程進入阻塞狀態(tài)。

鎖升級的路徑

最后,看下鎖的升級路徑,如圖所示。從無鎖到偏向鎖,再到輕量級鎖,最后到重量級鎖。結(jié)合前面的知識,偏向鎖性能最好,避免了 CAS 操作。而輕量級鎖利用自旋和 CAS 避免了重量級鎖帶來的線程阻塞和喚醒,性能中等。重量級鎖則會把獲取不到鎖的線程阻塞,性能最差。

JVM 默認會優(yōu)先使用偏向鎖,如果有必要的話才逐步升級,這大幅提高了鎖的性能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容