synchronized內置鎖原理

来源:https://www.cnblogs.com/pluto-charon/archive/2020/05/01/12815349.html
-Advertisement-
Play Games

前言 併發編程是java中不可或缺的模塊。與串列程式相比,它們能使複雜的非同步代碼變得簡單,從而極大地簡化了複雜系統的開發。此外,想要充分發揮多處理器系統的強大計算能力,最簡單的方式就是使用線程。隨著處理器數量的持續增長,如何高效地使用蝙蝠正變得越來越重要。同時在當今互聯網的時代,大量的互聯網應用都面 ...


前言

併發編程是java中不可或缺的模塊。與串列程式相比,它們能使複雜的非同步代碼變得簡單,從而極大地簡化了複雜系統的開發。此外,想要充分發揮多處理器系統的強大計算能力,最簡單的方式就是使用線程。隨著處理器數量的持續增長,如何高效地使用蝙蝠正變得越來越重要。同時在當今互聯網的時代,大量的互聯網應用都面對著海量的訪問請求,因此,併發編程在我們的應用中占的比重越來越大。

為什麼會有併發安全

在講併發編程前,我們先來看段代碼:

public class UnsafeDemo {

    Integer iCount = 0;

    /**聲明不變對象,用於加鎖*/
    final Object obj = new Object();
    
    /**
     * 加1操作
     */
    public void addCount() {
        iCount++;
    }
    
    /**
     * 使用內置鎖 --加1操作
     */
    public void addCount1() {
        synchronized (obj){
            iCount++;
        }
    }

    /**
     * 獲取count的值
     *
     * @return int
     */
    public int getCount() {
        return iCount;
    }

    /**
     * 啟動方法
     *
     * @param args 參數
     */
    public static void main(String[] args) throws InterruptedException {
        UnsafeDemo unsafeDemo = new UnsafeDemo();
        //新建2個線程執行+1的方法
        Thread thread1 = new CountAddThread(unsafeDemo);
        Thread thread2 = new CountAddThread(unsafeDemo);
        thread1.start();
        thread2.start();

        //阻塞等待程式執行
        Thread.sleep(1000);
        System.out.println("count進行多線程相加後的結果:"+unsafeDemo.getCount());
        /**
         * count進行多線程相加後的結果:17296
         * count進行多線程相加後的結果:11913
         * count進行多線程相加後的結果:10375
         */
    }

    /**
     * 定義一個私有的線程類
     */
    private static class CountAddThread extends Thread {

        private UnsafeDemo unsafeDemo;

        public CountAddThread(UnsafeDemo unsafeDemo) {
            this.unsafeDemo = unsafeDemo;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                unsafeDemo.addCount();
            }
        }
    }

}

按照我們程式的思路,輸出的結果應該是20000,但實際上,輸出的結果並不確定。因為線程的運行是靠CPU來隨機決定的;i++是非線程安全的,非原子性的操作(一共分3步,獲取i的值,將i的值加1,將結果寫入到i中);

線程1首先將元空間的iCount值讀取到線程1的本地緩存中,然後線程1將iCount的值進行改變,然後再將改變後的iCount的值寫入到元空間中。線上程1執行的時候,可能線程2也在進行同樣的操作,就出現了值覆蓋的情況。由此可以引出併發安全的本質:

  1. 原子性:在一個操作中,cpu不可以在中途暫停然後再執行,即不可以被中斷操作,要麼執行完成,要麼不執行
  2. 可見性:必須確保釋放鎖之前對共用數據做出的更改對於隨後獲得該鎖的另一個線程是可見的 。(volatile只保證可見性不保證原子性)
  3. 有序性:線程之間必須是有序的訪問共用變數

對於這種情況,就需要對程式加鎖操作。今天我們就來聊聊synchronized內置鎖。

synchronized使用

synchronized是Java提供的一個併發控制的關鍵字,可以修飾一個類、修飾一個方法、修飾代碼塊、修飾靜態代碼。保證了代碼的原子性和可見性以及有序性,但是不會處理重排序以及代碼優化的過程,但是在一個線程中執行肯定是有序的,因此是有序的。

synchronized原理

/**
* 對加1操作加上synchronized內置鎖,這樣在執行的時候,就能達到我們理想的效果,輸出結果為20000
*/
public void addCount() {
    synchronized (this){
   		 iCount++;
    }
}

那為什麼synchronized能解決原子性,可見性,有序性這幾個問題呢?

先來瞭解一下幾個名詞:

無鎖狀態:沒有加鎖

偏向鎖:同步代碼塊第一次進入的時候,會生成偏向鎖

輕量級鎖:偏向鎖的線程沒有結束,此時又有其他的線程進入,進行鎖升級,其他線程自旋

重量級鎖:輕量級鎖沒有釋放,其他線程一直休眠沒有獲取鎖。重量級鎖是依賴對象內部的monitor鎖來實現的,而monitor又依賴操作系統的MutexLock(互斥鎖)來實現的,所以重量級鎖也稱為互斥鎖。

自旋:cpu空跑,讓等待的線程不放棄cpu執行時間,而是執行一個自旋(一般是空迴圈);即do{ }while(自旋的次數) 迴圈。自旋次數可以通過參數 -XX:PreBlockSpin 來修改。

鎖消除:虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共用數據競爭的鎖進行消除。一般根據逃逸分析的數據支持來作為判定依據。

在瞭解鎖的時候,首先需要瞭解java的對象頭,所有的鎖的實現都依賴於對象頭。

以32位的操作系統為例,對象頭的運行時數據(mark word)的預設存儲結構如下表所示:

回到上面的例子,來查看synchronized的原理,理解鎖膨脹的過程(即synchronized內置鎖的原理):

1.無鎖狀態:預設偏向鎖為0;

2.偏向鎖:當有一個線程進入同步代碼塊,將鎖對象頭的hashCode改成線程1的ID,同時標誌成偏向鎖。同時線程2也訪問到了同步代碼塊,發現鎖對象頭的ID並不是自己的ID;但是線程2還是要得到得到鎖,於是就去嘗試修改對象頭的HashCode值。

3.輕量級鎖:如果線程1剛好釋放鎖,那線程2就能修改成功,獲得鎖。但如果線程1一直持有鎖,線程2修改失敗,那線程2就進行鎖消除,同時虛擬機對線程1的鎖升級為輕量級鎖。

4.重量級鎖:虛擬機會有線程1和線程2分配一塊各自的記憶體空間,並且把鎖複製到各自的空間中,通知將鎖狀態變成空的。同時把鎖狀態變成線程對應的ID。此時線程2進入自旋(不釋放CPU),當線程2自旋到一定的程度(上面的自旋次數),線程2進入睡眠狀態,釋放CPU。

線程1執行完畢,釋放輕量級鎖,這時候發現,鎖對象的指針並不是指向自己,開始釋放鎖,並喚醒所有休眠的線程。

CAS(Compare And Swap)機制

要談CAS機制,還是先來段代碼,跟前文的代碼差別不大。就是將Int換成了AtomicInteger,將addCount和getCount方法改變了一下。可以看到執行出來的結果,跟使用synchronized的效果一樣。並且在某些情況下,代碼的性能會比synchronized更好。

public class VolatileDemo {

    AtomicInteger count =new AtomicInteger(0);

    public void addCount(){
        count.incrementAndGet();//i++
    }

    public int getCount(){
        return count.get();
    }

    /**
     * 啟動方法
     *
     * @param args 參數
     */
    public static void main(String[] args) throws InterruptedException {
        VolatileDemo volatileDemo = new VolatileDemo();
        //新建2個線程執行+1的方法
        Thread thread1 = new VolatileDemo.CountAddThread(volatileDemo);
        Thread thread2 = new VolatileDemo.CountAddThread(volatileDemo);
        thread1.start();
        thread2.start();

        //阻塞等待程式執行
        Thread.sleep(100);
        System.out.println("count進行多線程相加後的結果:"+volatileDemo.getCount());
        /**
         * count進行多線程相加後的結果:20000
         * count進行多線程相加後的結果:20000
         * count進行多線程相加後的結果:20000
         */
    }

    /**
     * 定義一個私有的線程類
     */
    private static class CountAddThread extends Thread {

        private VolatileDemo volatileDemo;

        public CountAddThread( VolatileDemo volatileDemo) {
            this.volatileDemo = volatileDemo;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                volatileDemo.addCount();
            }
        }
    }
}

來看看AtomicInteger#incrementAndGet()的源碼:

/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
	return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

可以看到getAndAddInt()裡面是使用了do{ }while()迴圈,也就是一個自旋。調用的compareAndSwapInt()是使用native修飾的,去記憶體中查詢新值和舊值並比較。

而Atomic操作類的底層正是用到了“CAS機制”。上文講到的自旋也是使用了CAS機制。CAS的核心是利用Unsafe對象實現的。Unsafe包是用來幫助java訪問操作系統底層資源的類。通過Unsafe,java具有了操作底層的能力,可以提升運行效率。

那麼什麼是CAS機制呢?

CAS機制中使用了3個基本操作數:記憶體地址V,舊的預期值A,要修改的新值B。

更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B。否則不進行任何操作。

從思想上來說,synchronized屬於悲觀鎖,悲觀的認為程式中的併發情況嚴重,所以嚴防死守,CAS屬於樂觀鎖,樂觀地認為程式中的併發情況不那麼嚴重,所以讓線程不斷去重試更新。

CAS機制的優點:

  1. 直接操作底層,在併發量小的時候,可以提搞效率

CAS機制的缺點:

  1. ​ 迴圈開銷大
  2. 只能保證單獨共用變數的原子操作
  3. ABA問題,當一個值從A變成B,又更新回A,普通CAS機制會誤判通過檢測。利用版本號比較可以有效解決ABA問題。

引用:

https://www.zhihu.com/question/39009953/answer/80186008

https://cnblogs.com/kismetv/p/10787228.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們通常下載文件的方式無非後端給一個生成文件鏈接, 前端通過a標簽或者iframe的方式去下載,這種方式的弊端是無法監測到文件是否下載完成,無法給用戶友好的提示,以避免用戶短時間內重覆點擊下載. 如果我們能用Ajax從後端拿到PDF的相關數據,再在前端下載成PDF就可以解決這個問題,那麼新的問題是: ...
  • 項目背景:在自己的主頁中添加一個百度搜索框,在裡面輸入要搜索的內容後可以直接跳轉到相關內容搜索結果的界面。搜索框是用form表單實現的,action中為百度的url,將輸入的內容拼接到url中,以實現直接跳轉到搜索結果界面。 1.觀察在百度中搜索內容時的url,打開百度一下,輸入搜索內容,如123 ...
  • 背景 在做Electron Windows 桌面應用時候,做滑鼠懸浮到托盤圖標上時顯示一個懸浮框(例如做消息提醒),但因為Windows沒有提供托盤mouse-enter/mouse-leave事件,無法直接做這個功能,考慮到還有mouse-move事件,弄個間接的方式實現。 實現步驟 1、監聽mo ...
  • 今天看著視頻學習的時候遇到了,覺得以後會遇到,就記錄下來了 需求:點擊圖片,顯示遮擋層,同時下滑顯示視頻。點擊"X",遮擋層消失,同時視頻上滑隱藏。 具體代碼: 通過v-show控制整個video-box盒子的顯示與隱藏,可以提升性能。 <div class="item-video"> <h2> 6 ...
  • JavaScript實現無縫輪播圖: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0 ...
  • flex 佈局: 水平主軸:水平主軸開始位置到結束位置 垂直交叉軸:垂直交叉軸開始位置到結束位置 flex佈局屬性: flex佈局屬性: 屬性名稱 屬性值 解釋 備註 flex-direction(伸縮流方向) row 按行排列 從左到右 row-reverse 按行反向排列 從右到左 column ...
  • 六大設計原則 單一職責 定義 每個類都應該有一個單一的功能 一個類或者模塊應該有且只有一個改變的原因 規範 定義類的方法 避免類之間耦合度太高 里氏替換 定義 只要有父類出現的地方,都可以用子類來替代 規範 不要破壞繼承體系 增加子類的時候系統可以正常執行 依賴倒置原則 定義 高層模塊不應該依賴底層 ...
  • OO第二階段(5-8周)PTA作業總結 0.前言 本次博客針對的是PTA第二階段的作業,這次作業相比於第一階段來講難度、思維高度都提高了很多,耗費的精力和時間也很多但是收穫很大;這次作業很大一部分難點及側重點是在正則表達式上,這一部分確實難學,你要對程式的需求分析精準,不斷修改正則才能達到預期結果; ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...