java自旋鎖和JVM對(duì)鎖的優(yōu)化詳解
背景
先上圖
由此可見,非自旋鎖如果拿不到鎖會(huì)把線程阻塞,直到被喚醒;
自旋鎖拿不到鎖會(huì)一直嘗試
為什么要這樣?
好處
阻塞和喚醒線程都是需要高昂的開銷的,如果同步代碼塊中的內(nèi)容不復(fù)雜,那么可能轉(zhuǎn)換線程帶來(lái)的開銷比實(shí)際業(yè)務(wù)代碼執(zhí)行的開銷還要大。
在很多場(chǎng)景下,可能我們的同步代碼塊的內(nèi)容并不多,所以需要的執(zhí)行時(shí)間也很短,如果我們僅僅為了這點(diǎn)時(shí)間就去切換線程狀態(tài),那么其實(shí)不如讓線程不切換狀態(tài),而是讓它自旋地嘗試獲取鎖,等待其他線程釋放鎖,有時(shí)我只需要稍等一下,就可以避免上下文切換等開銷,提高了效率。
用一句話總結(jié)自旋鎖的好處,那就是自旋鎖用循環(huán)去不停地嘗試獲取鎖,讓線程始終處于 Runnable 狀態(tài),節(jié)省了線程狀態(tài)切換帶來(lái)的開銷。
AtomicLong的實(shí)現(xiàn)
getAndIncrement方法
public final long getAndIncrement() { return unsafe.getAndAddLong(this, valueOffset, 1L); }
public final long getAndAddLong(Object o, long offset, long delta) { long v; do { v = getLongVolatile(o, offset); //如果修改過(guò)程中遇到其他線程競(jìng)爭(zhēng)導(dǎo)致沒修改成功,死循環(huán),直到修改成功為止 } while (!compareAndSwapLong(o, offset, v, v + delta)); return v; }
實(shí)驗(yàn)
package com.reflect; import java.util.concurrent.atomic.AtomicReference; class ReentrantSpinLock { private AtomicReference<Thread> owner = new AtomicReference<>(); private int count = 0; public void lock() { Thread t = Thread.currentThread(); if (t == owner.get()) { ++count; return; } while (!owner.compareAndSet(null, t)) { System.out.println("自旋了"); } } public void unlock() { Thread t = Thread.currentThread(); if (t == owner.get()) { if (count > 0) { --count; } else { owner.set(null); } } } public static void main(String[] args) { ReentrantSpinLock spinLock = new ReentrantSpinLock(); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "開始嘗試獲 取自旋鎖"); spinLock.lock(); try { System.out.println(Thread.currentThread().getName() + "獲取到 了自旋鎖"); Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } finally { spinLock.unlock(); System.out.println(Thread.currentThread().getName() + "釋放了 了自旋鎖"); } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } }
很多"自旋了",說(shuō)明自旋期間CPU依然在不停運(yùn)轉(zhuǎn)
缺點(diǎn)
雖然避免了線程切換的開銷,但是在避免線程切換開銷的同時(shí)帶來(lái)新的開銷:不停嘗試獲取鎖,如果這個(gè)鎖一直不能被釋放那么這種嘗試知識(shí)無(wú)用的嘗試,浪費(fèi)處理器資源,就是說(shuō)一開始自旋鎖開銷低于線程切換,但是隨著時(shí)間增加,這種開銷后期甚至超過(guò)線程切換的開銷,得不償失
適用場(chǎng)景
- 并發(fā)不是特別高的場(chǎng)景
- 臨界區(qū)比較短小的情況,利用避免線程切換提高效率
如果臨界區(qū)很大,線程拿到鎖很久才釋放,那自旋會(huì)一直占用CPU但無(wú)法拿到鎖,浪費(fèi)資源
JVM對(duì)鎖做了哪些優(yōu)化?
相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虛擬機(jī)對(duì) synchronized 內(nèi)置鎖的性能進(jìn)行了很多優(yōu)化,包括自適應(yīng)的自旋、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖等。有了這些優(yōu)化措施后,synchronized 鎖的性能得到了大幅提高,下面我們分別介紹這些具體的優(yōu)化。
自適應(yīng)的自旋鎖
在 JDK 1.6 中引入了自適應(yīng)的自旋鎖來(lái)解決長(zhǎng)時(shí)間自旋的問(wèn)題。自適應(yīng)意味著自旋的時(shí)間不再固定,而是會(huì)根據(jù)最近自旋嘗試的成功率、失敗率,以及當(dāng)前鎖的擁有者的狀態(tài)等多種因素來(lái)共同決定。自旋的持續(xù)時(shí)間是變化的,自旋鎖變 “聰明” 了。比如,如果最近嘗試自旋獲取某一把鎖成功了,那么下一次可能還會(huì)繼續(xù)使用自旋,并且允許自旋更長(zhǎng)的時(shí)間;但是如果最近自旋獲取某一把鎖失敗了,那么可能會(huì)省略掉自旋的過(guò)程,以便減少無(wú)用的自旋,提高效率。
鎖消除
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; public Person getPerson() { return new Person(person); } public void printEmployeeDetail(Employee emp) { Person person = emp.getPerson(); System.out.println("Employee's name: " + person.getName() + "; age: " + person.getAge()); } }
在這段代碼中,我們看到下方的 Employee 類中的 getPerson() 方法,這個(gè)方法中使用了類里面的person 對(duì)象,并且新建一個(gè)和它屬性完全相同的新的 person 對(duì)象,目的是防止方法調(diào)用者修改原來(lái)的 person 對(duì)象。但是在這個(gè)例子中,其實(shí)是沒有任何必要新建對(duì)象的,因?yàn)槲覀兊膒rintEmployeeDetail() 方法沒有對(duì)這個(gè)對(duì)象做出任何的修改,僅僅是打印,既然如此,我們其實(shí)可以直接打印最開始的 person 對(duì)象,而無(wú)須新建一個(gè)新的。
如果編譯器可以確定最開始的 person 對(duì)象不會(huì)被修改的話,它可能會(huì)優(yōu)化并且消除這個(gè)新建 person的過(guò)程。根據(jù)這樣的思想,接下來(lái)我們就來(lái)舉一個(gè)鎖消除的例子,,經(jīng)過(guò)逃逸分析之后,如果發(fā)現(xiàn)某些對(duì)象不可能被其他線程訪問(wèn)到,那么就可以把它們當(dāng)成棧上數(shù)據(jù),棧上數(shù)據(jù)由于只有本線程可以訪問(wèn),自然是線程安全的,也就無(wú)需加鎖,所以會(huì)把這樣的鎖給自動(dòng)去除掉。
例如,我們的 StringBuffffer 的 append 方法如下所示:
@Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String.valueOf(obj)); return this; }
從代碼中可以看出,這個(gè)方法是被 synchronized 修飾的同步方法,因?yàn)樗赡軙?huì)被多個(gè)線程同時(shí)使用。
但是在大多數(shù)情況下,它只會(huì)在一個(gè)線程內(nèi)被使用,如果編譯器能確定這個(gè) StringBuffffer 對(duì)象只會(huì)在一個(gè)線程內(nèi)被使用,就代表肯定是線程安全的,那么我們的編譯器便會(huì)做出優(yōu)化,把對(duì)應(yīng)的synchronized 給消除,省去加鎖和解鎖的操作,以便增加整體的效率。
鎖粗化
釋放了鎖,緊接著什么都沒做,又重新獲取鎖
public void lockCoarsening() { synchronized (this) { } synchronized (this) { } synchronized (this) { } }
那么其實(shí)這種釋放和重新獲取鎖是完全沒有必要的,如果我們把同步區(qū)域擴(kuò)大,也就是只在最開始加一次鎖,并且在最后直接解鎖,那么就可以把中間這些無(wú)意義的解鎖和加鎖的過(guò)程消除,相當(dāng)于是把幾個(gè)synchronized 塊合并為一個(gè)較大的同步塊。這樣做的好處在于在線程執(zhí)行這些代碼時(shí),就無(wú)須頻繁申請(qǐng)與釋放鎖了,這樣就減少了性能開銷。
不過(guò),我們這樣做也有一個(gè)副作用,那就是我們會(huì)讓同步區(qū)域變大。如果在循環(huán)中我們也這樣做,如代碼所示:
for (int i = 0; i < 1000; i++) { synchronized (this) { } }
也就是我們?cè)诘谝淮窝h(huán)的開始,就開始擴(kuò)大同步區(qū)域并持有鎖,直到最后一次循環(huán)結(jié)束,才結(jié)束同步代碼塊釋放鎖的話,這就會(huì)導(dǎo)致其他線程長(zhǎng)時(shí)間無(wú)法獲得鎖。所以,這里的鎖粗化不適用于循環(huán)的場(chǎng)景,僅適用于非循環(huán)的場(chǎng)景。
鎖粗化功能是默認(rèn)打開的,用 -XX:-EliminateLocks可以關(guān)閉該功能
偏向鎖/ 輕量級(jí)鎖/ 重量級(jí)鎖
這三種鎖是特指 synchronized 鎖的狀態(tài),通過(guò)在對(duì)象頭中的 mark word 來(lái)表明鎖的狀態(tài)
- 偏向鎖
對(duì)于偏向鎖而言,它的思想是如果自始至終,對(duì)于這把鎖都不存在競(jìng)爭(zhēng),那么其實(shí)就沒必要上鎖,只要打個(gè)標(biāo)記就行了。一個(gè)對(duì)象在被初始化后,如果還沒有任何線程來(lái)獲取它的鎖時(shí),它就是可偏向的,當(dāng)有第一個(gè)線程來(lái)訪問(wèn)它嘗試獲取鎖的時(shí)候,它就記錄下來(lái)這個(gè)線程,如果后面嘗試獲取鎖的線程正是這個(gè)偏向鎖的擁有者,就可以直接獲取鎖,開銷很小。
- 輕量級(jí)鎖
JVM 的開發(fā)者發(fā)現(xiàn)在很多情況下,synchronized 中的代碼塊是被多個(gè)線程交替執(zhí)行的,也就是說(shuō),并不存在實(shí)際的競(jìng)爭(zhēng),或者是只有短時(shí)間的鎖競(jìng)爭(zhēng),用 CAS 就可以解決。這種情況下,重量級(jí)鎖是沒必要的。輕量級(jí)鎖指當(dāng)鎖原來(lái)是偏向鎖的時(shí)候,被另一個(gè)線程所訪問(wèn),說(shuō)明存在競(jìng)爭(zhēng),那么偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,線程會(huì)通過(guò)自旋的方式嘗試獲取鎖,不會(huì)阻塞
- 重量級(jí)鎖
這種鎖利用操作系統(tǒng)的同步機(jī)制實(shí)現(xiàn),所以開銷比較大。當(dāng)多個(gè)線程直接有實(shí)際競(jìng)爭(zhēng),并且鎖競(jìng)爭(zhēng)時(shí)間比較長(zhǎng)的時(shí)候,此時(shí)偏向鎖和輕量級(jí)鎖都不能滿足需求,鎖就會(huì)膨脹為重量級(jí)鎖。重量級(jí)鎖會(huì)讓其他申請(qǐng)卻拿不到鎖的線程進(jìn)入阻塞狀態(tài)。
鎖升級(jí)
偏向鎖性能最好,避免了 CAS 操作。而輕量級(jí)鎖利用自旋和 CAS 避免了重量級(jí)鎖帶來(lái)的線程阻塞和喚醒,性能中等。重量級(jí)鎖則會(huì)把獲取不到鎖的線程阻塞,性能最差。
JVM 默認(rèn)會(huì)優(yōu)先使用偏向鎖,如果有必要的話才逐步升級(jí),這大幅提高了鎖的性能
以上就是java自旋鎖和JVM對(duì)鎖的優(yōu)化詳解的詳細(xì)內(nèi)容,更多關(guān)于java自旋鎖JVM對(duì)鎖優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Spring boot Admin 使用eureka監(jiān)控服務(wù)
本篇文章主要介紹了詳解Spring boot Admin 使用eureka監(jiān)控服務(wù),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12Java按照List內(nèi)存儲(chǔ)的對(duì)象的某個(gè)字段進(jìn)行排序的實(shí)例
下面小編就為大家?guī)?lái)一篇Java按照List內(nèi)存儲(chǔ)的對(duì)象的某個(gè)字段進(jìn)行排序的實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12IDEA 連接數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方法
這篇文章主要介紹了IDEA 連接數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10MyBatis傳入多個(gè)參數(shù)時(shí)parameterType的寫法
這篇文章主要介紹了MyBatis傳入多個(gè)參數(shù)時(shí)parameterType的寫法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12SpringCloud添加客戶端Eureka Client過(guò)程解析
這篇文章主要介紹了SpringCloud添加客戶端Eureka Client過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Mybatis批量插入大量數(shù)據(jù)的最優(yōu)方式總結(jié)
批量插入功能是我們?nèi)粘9ぷ髦斜容^常見的業(yè)務(wù)功能之一,下面這篇文章主要給大家總結(jié)介紹了關(guān)于Mybatis批量插入大量數(shù)據(jù)的幾種最優(yōu)方式,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03java實(shí)現(xiàn)學(xué)生教師管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)學(xué)生教師管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10