日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java中的锁 | JDK6 关于锁的优化

發布時間:2024/8/1 java 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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就會將線程掛起。
  • JDK6之前: 自旋的次數通過JVM參數 -XX:PreBlockSpin 設置,但是開發者往往不知道該設置多少比較合適
  • 于是在JDK6中: 對其進行了優化,加入了“自適應自旋鎖”。
  • 自適應自旋鎖的大致原理 :

  • 線程如果自旋成功了,那么下次自旋的最大次數會增加,因為JVM認為既然上次成功了,那么這一次也很大概率會成功。
  • 反之,如果很少會自旋成功,那么下次會減少自旋的次數甚至不自旋,避免CPU空轉。

  • 2.5 鎖膨脹

    在JDK6之前,Synchronized用的都是重量級鎖,依賴于OS的Mutex Lock來實現,OS將線程從用戶態切換到核心態,成本非常高,性能很低。

    在JDK6中,針對鎖進行優化,不直接使用重量級鎖,而是逐步進行鎖的膨脹
    鎖狀態的級別由低到高為:無鎖、偏向鎖、輕量級鎖、重量級鎖。
    偏向鎖、輕量級鎖都屬于樂觀鎖,重量級鎖屬于悲觀鎖。

    默認為無鎖狀態,隨著鎖競爭的激烈程度會不斷膨脹,最終才會使用開銷最大的重量級鎖。


    2.5.1 無鎖

    在對象的頭信息Mark Word中記錄了對象的鎖狀態,如下圖:


    如果沒有任何線程競爭鎖,那么對象默認為無鎖狀態。


    2.5.2 偏向鎖

    針對單線程鎖競爭做的優化,最樂觀的鎖。

    HotSpot作者經過研究發現,開發者為了保證線程安全問題給代碼塊上了鎖,但是大多數情況下,鎖并不存在多線程競爭,而是單線程反復獲得。

    單一線程,為什么還要去頻繁的獲取和釋放鎖呢?所以就有了“偏向鎖”的概念。


    偏向鎖是針對單線程反復獲得鎖而做的優化,是最樂觀的鎖:只有單個線程來競爭鎖。

    在JDK5中偏向鎖是關閉的,JDK6中默認開啟,可以通過JVM參數-XX:-UseBiasedLocking來關閉偏向鎖。


    偏向鎖大致流程如下:

  • 線程A第一次獲得鎖后,CAS操作修改對象頭信息中的Mark Word:無鎖->偏向鎖、偏向線程ID->線程A。
  • 線程A需要再次獲得鎖時,首先判斷偏向線程ID是否是自己,如果是則直接獲得鎖,速度非常快。
    偏向鎖并不會主動釋放,需要等待其他線程來競爭。
    線程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 关于锁的优化的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。