Java中的锁 | JDK6 关于锁的优化
Java中的鎖 | JDK6 關于鎖的優化
1. JDK1.6之前的鎖是什么樣的
在JDK6之前,Synchronized是非常笨重的,以至于開發者不太愿意使用而慢慢摒棄它
但是在JDK6中,對Synchronized做了大量的優化,性能和ReentrantLock已經不相上下,官方也更加推薦使用Synchronized。
2. JDK1.6之后的優化
2.1 鎖消除
設計一個類時,為了考慮并發安全,往往會對代碼塊上鎖。
但是有時候壓根就不會產生并發問題
例如:在線程私有的棧內存中使用線程安全的類實例,且實例不存在逃逸。
如果不存在并發安全,那還有什么理由上鎖呢?
在 JIT 編譯時,會對運行上下文進行掃描,去除不可能產生并發問題的鎖。
用代碼舉例:
public String method(){StringBuffer sb = new StringBuffer();sb.append("1");sb.append("2");return sb.toString(); }如上代碼,StringBuffer的append()方法被synchronized修飾,但是在該方法中不存在并發問題,方法棧內存為線程私有,sb實例不可能被其他線程訪問到,對于這種情況就會進行鎖消除。
2.2 鎖粗化
由于鎖的競爭和釋放開銷比較大,如果代碼中對鎖進行了頻繁的競爭和釋放,那么JVM會進行優化,將鎖的范圍適當擴大。
如下代碼,在循環內使用synchronized,JVM鎖粗化后,會將鎖范圍擴大到循環外面
public String method(){for (int i= 0; i < 100; i++) {synchronized (this){...}} }2.3 自旋鎖
正常情況下線程阻塞的話需要切換至內核態,但是優化后的處理方式如下:
線程阻塞不必直接轉化為內核態, 嘗試自旋可以節省下來切換成內核態(因為用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束后切換回用戶態繼續工作。)所需要的時間。
Question : 當有多個線程在競爭同一把鎖時,競爭失敗的線程如何處理?
面對這種情況有兩種選擇:
如果鎖競爭非常激烈,且短時間得不到釋放,那么將線程掛起效率會更高,因為競爭失敗的線程不斷自旋會造成CPU空轉,浪費性能。
2.3.1 自旋鎖優劣勢
優勢:
如果鎖競爭并不激烈,且鎖會很快得到釋放,那么自旋效率會更高。因為將線程掛起和喚醒是一個開銷很大的操作。
自旋鎖的優化是針對“鎖競爭不激烈,且會很快釋放”的場景,避免了OS頻繁掛起和喚醒線程。
缺點:
但是線程自旋是需要消耗CPU的,說白了就是讓CPU在做無用功,如果一直獲取不到鎖,那線程也不能一直占用CPU自旋做無用功,所以需要設定一個自旋等待的最大時間.
2.4 自適應自旋鎖
當線程競爭鎖失敗時,自旋和掛起哪一種更高效?
自適應自旋鎖 解決的就是這個問題
策略:
- 當線程競爭鎖失敗時,會自旋N次,如果仍然競爭不到鎖,說明鎖競爭比較激烈,繼續自旋會浪費性能,JVM就會將線程掛起。
自適應自旋鎖的大致原理 :
2.5 鎖膨脹
在JDK6之前,Synchronized用的都是重量級鎖,依賴于OS的Mutex Lock來實現,OS將線程從用戶態切換到核心態,成本非常高,性能很低。
在JDK6中,針對鎖進行優化,不直接使用重量級鎖,而是逐步進行鎖的膨脹。
鎖狀態的級別由低到高為:無鎖、偏向鎖、輕量級鎖、重量級鎖。
偏向鎖、輕量級鎖都屬于樂觀鎖,重量級鎖屬于悲觀鎖。
默認為無鎖狀態,隨著鎖競爭的激烈程度會不斷膨脹,最終才會使用開銷最大的重量級鎖。
2.5.1 無鎖
在對象的頭信息Mark Word中記錄了對象的鎖狀態,如下圖:
如果沒有任何線程競爭鎖,那么對象默認為無鎖狀態。
2.5.2 偏向鎖
針對單線程鎖競爭做的優化,最樂觀的鎖。
HotSpot作者經過研究發現,開發者為了保證線程安全問題給代碼塊上了鎖,但是大多數情況下,鎖并不存在多線程競爭,而是單線程反復獲得。
單一線程,為什么還要去頻繁的獲取和釋放鎖呢?所以就有了“偏向鎖”的概念。
偏向鎖是針對單線程反復獲得鎖而做的優化,是最樂觀的鎖:只有單個線程來競爭鎖。
在JDK5中偏向鎖是關閉的,JDK6中默認開啟,可以通過JVM參數-XX:-UseBiasedLocking來關閉偏向鎖。
偏向鎖大致流程如下:
偏向鎖并不會主動釋放,需要等待其他線程來競爭。
線程B來競爭鎖,發現鎖偏向線程A,此時CAS操作失敗,則進一步判斷:線程A是否還在占用鎖?
- 線程A未占用:將鎖重新偏向線程B,線程B獲得鎖。
- 線程A仍占用:說明鎖存在多線程競爭,升級為:輕量級鎖。
2.5.3 輕量級鎖
針對鎖競爭不激烈做的優化,使用自旋鎖避免線程頻繁掛起和喚醒。
只有單一線程競爭鎖時用的是偏向鎖,最樂觀的鎖也是性能最高的鎖。
一旦涉及到多線程競爭鎖,就會升級為輕量級鎖。
偏向鎖發現線程不一致, 則升級為輕量級鎖
- 輕量級鎖認為:存在多線程競爭鎖,但是競爭不激烈。
- 輕量級鎖的實現原理:讓競爭鎖失敗的線程自旋而不是掛起。
如果將競爭鎖失敗的線程直接掛起,然后鎖釋放后再將其喚醒,這是一個開銷很大的操作。
而大多數情況下,鎖的占用時間往往非常短,會很快被釋放,那么輕量級鎖認為:不要掛起線程,而是讓其進行自旋,執行一些無用的指令,只要鎖被釋放,線程馬上就能獲得鎖,而不用等待OS將其喚醒。
總結:自旋成本 < 線程掛起成本
線程A獲得鎖未釋放,此時線程B來競爭鎖,發現鎖被線程A占用,線程B認為線程A可能很快就會釋放鎖,于是進行自旋操作:
- 自旋成功:說明鎖的占用時間并不長,下次會自適應增加最大自旋次數(自適應自旋)。
- 自旋失敗:鎖的占用時間較長,繼續自旋會浪費CPU資源,線程被掛起,升級為:重量級鎖。
2.5.4 重量級鎖
開銷最大,性能最低的悲觀鎖,鎖競爭激烈時采用
- 鎖競爭不激烈時,競爭鎖失敗的線程進行自旋而非掛起可以提升性能,因為 自旋的開銷 < 線程掛起、喚醒的開銷。
- 但是鎖競爭激烈時,自旋會造成更大的資源開銷。
例如:100個線程競爭同一把鎖,99個線程在自旋,意味著99%的CPU資源被浪費,此時自旋的開銷>線程掛起、喚醒的開銷。
當競爭比較激烈時,就會膨脹為重量級鎖,因為輕量級鎖的效率此時更低。
重量級鎖通過監視器鎖(Monitor)實現,Monitor又依賴于底層OS的Mutex Lock實現。
升級為重量級鎖后,所有競爭鎖失敗的線程都會被阻塞掛起,鎖被釋放后再將線程喚醒。
線程頻繁的掛起和喚醒,OS需要將線程從用戶態切換為核心態,這個操作成本是非常高的,需要花費較長的時間,這就導致重量級鎖效率很低。
3. JDK6后Sychronized和Lock性能比較
在Synchronized和Lock的區別中已經說過,在不同場景下兩者的性能表現不同。
盡管JDK6為Synchronized做了大量優化,但是在競爭比較激烈時,Synchronized的性能依然會有所下降。
而Lock不管鎖競爭激烈與否,性能基本保持在一個數量級,適合鎖競爭比較激烈的應用場景。
分別對Synchronized和Lock進行性能測試,1、10、100線程下分別進行1億次自增運算,采樣5次。
測試代碼:
public abstract class PerformanceTemplate {protected int threadCount = 0;//線程數protected int index = 0;protected final int count;protected long startTime = System.currentTimeMillis();private final CyclicBarrier cb;public PerformanceTemplate(int count, int threadCount) {this.count = count;this.threadCount = threadCount;this.cb = new CyclicBarrier(threadCount);}public void test() {int c = count / threadCount;for (int i = 0; i < threadCount; i++) {new Thread(() -> {try {cb.await();} catch (Exception e) {e.printStackTrace();}while (true) {func();}}).start();}}protected abstract void func();protected void print(){System.out.println("耗時:" + (System.currentTimeMillis() - startTime)+"ms");startTime = System.currentTimeMillis();} }public class Sync extends PerformanceTemplate {public Sync(int count, int threadCount) {super(count, threadCount);}@Overrideprotected synchronized void func() {if (++index % count == 0) {print();}}@Overrideprotected void print() {System.out.print("Synchronized:1億次運算,"+threadCount+"線程耗時:");super.print();} }public class Lock extends PerformanceTemplate {private ReentrantLock lock = new ReentrantLock();public Lock(int count, int threadCount) {super(count, threadCount);}@Overrideprotected void func() {lock.lock();if (++index % count == 0) {print();}lock.unlock();}@Overrideprotected void print() {System.out.print("Lock:1億次運算,"+threadCount+"線程耗時:");super.print();} }測試結果:
Synchronized:1億次運算,1線程耗時:耗時:3174ms Synchronized:1億次運算,1線程耗時:耗時:1878ms Synchronized:1億次運算,1線程耗時:耗時:2404ms Synchronized:1億次運算,1線程耗時:耗時:2392ms Synchronized:1億次運算,1線程耗時:耗時:2409ms --- Synchronized:1億次運算,10線程耗時:耗時:4835ms Synchronized:1億次運算,10線程耗時:耗時:5407ms Synchronized:1億次運算,10線程耗時:耗時:5391ms Synchronized:1億次運算,10線程耗時:耗時:5406ms Synchronized:1億次運算,10線程耗時:耗時:5462ms --- Synchronized:1億次運算,100線程耗時:耗時:4538ms Synchronized:1億次運算,100線程耗時:耗時:4921ms Synchronized:1億次運算,100線程耗時:耗時:4957ms Synchronized:1億次運算,100線程耗時:耗時:4999ms Synchronized:1億次運算,100線程耗時:耗時:4980ms Lock:1億次運算,1線程耗時:耗時:1985ms Lock:1億次運算,1線程耗時:耗時:1961ms Lock:1億次運算,1線程耗時:耗時:1857ms Lock:1億次運算,1線程耗時:耗時:2138ms Lock:1億次運算,1線程耗時:耗時:1912ms --- Lock:1億次運算,10線程耗時:耗時:2986ms Lock:1億次運算,10線程耗時:耗時:2861ms Lock:1億次運算,10線程耗時:耗時:2792ms Lock:1億次運算,10線程耗時:耗時:2792ms Lock:1億次運算,10線程耗時:耗時:2773ms --- Lock:1億次運算,100線程耗時:耗時:3023ms Lock:1億次運算,100線程耗時:耗時:2743ms Lock:1億次運算,100線程耗時:耗時:2706ms Lock:1億次運算,100線程耗時:耗時:2714ms Lock:1億次運算,100線程耗時:耗時:2765ms可以看到,Synchronized經過優化之后,性能并不差,和Lock差不多,Lock性能稍微高一丟丟。
3.1 Sychornized和Lock兩者如何選擇?
Synchronized是Java內置的同步器,使用簡單,語法清晰易讀,性能也不差,而且便于JVM堆棧跟蹤,官方也表示Synchronized性能后期還有優化的余地,所以如果沒有特殊要求,建議盡量使用Synchronized。
雖然建議盡量使用Synchronized,但是它畢竟自身存在一些功能上的缺陷,例如:無法響應中斷,不支持鎖超時,不能采用公平鎖等等,如果確實需要這些高級特性,那么還是應該使用ReentrantLock(重進入鎖)。
總結
以上是生活随笔為你收集整理的Java中的锁 | JDK6 关于锁的优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: rapidxml学习
- 下一篇: Java面试题!5年经验Java程序员面