【漫畫】JAVA併發編程 如何解決原子性問題

来源:https://www.cnblogs.com/liuyanling/archive/2020/05/11/12872798.html
-Advertisement-
Play Games

原創聲明:本文轉載自公眾號【胖滾豬學編程】,轉載務必註明出處! 在 "併發編程BUG源頭" 文章中,我們初識了併發編程的三個bug源頭:可見性、原子性、有序性。在 "如何解決可見性和原子性" 文章中我們大致瞭解了可見性和有序性的解決思路,今天輪到最後一個大bug,那就是原子性。 知識回顧 鎖模型 J ...


原創聲明:本文轉載自公眾號【胖滾豬學編程】,轉載務必註明出處!

併發編程BUG源頭文章中,我們初識了併發編程的三個bug源頭:可見性、原子性、有序性。在如何解決可見性和原子性文章中我們大致瞭解了可見性和有序性的解決思路,今天輪到最後一個大bug,那就是原子性。

知識回顧

_1

鎖模型

_2
_3

JAVA中的鎖模型

鎖是一種通用的技術方案,Java 語言提供的 synchronized 關鍵字,就是鎖的一種實現。

  • synchronized 是獨占鎖/排他鎖(就是有你沒我的意思),但是註意!synchronized並不能改變CPU時間片切換的特點,只是當其他線程要訪問這個資源時,發現鎖還未釋放,所以只能在外面等待。
  • synchronized一定能保證原子性,因為被 synchronized 修飾某段代碼後,無論是單核 CPU 還是多核 CPU,只有一個線程能夠執行該代碼,所以一定能保證原子操作
  • synchronized也能夠保證可見性和有序性。根據前第二篇文章:Happens-Before 規則之管程中鎖的規則:對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。即前一個線程的解鎖操作對後一個線程的加鎖操作可見。綜合 Happens-Before 的傳遞性原則,我們就能得出前一個線程在臨界區修改的共用變數(該操作在解鎖之前),對後續進入臨界區(該操作在加鎖之後)的線程是可見的。- synchronized 關鍵字可以用來修飾靜態方法,非靜態方法,也可以用來修飾代碼塊

理論說完了,來點實際的吧!首先我們用synchronized 修飾非靜態方法來改寫第一章中原子性問題的那段代碼:

    private long count = 0;
    
    // 修飾非靜態方法 當修飾非靜態方法的時候,鎖定的是當前實例對象 this。
    // 當該類中有多個普通方法被Synchronized修飾(同步),那麼這些方法的鎖都是這個類的一個對象this。多個線程訪問這些方法時,如果這些線程調用方法時使用的是同一個該類的對象,雖然他們訪問不同方法,但是他們使用同一個對象來調用,那麼這些方法的鎖就是一樣的,就是這個對象,那麼會造成阻塞。如果多個線程通過不同的對象來調用方法,那麼他們的鎖就是不一樣的,不會造成阻塞。
    private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchronized2 test = new TestSynchronized2();
        // 創建兩個線程,執行 add() 操作
        Thread th1 = new Thread(()->{
            test.add10K();
        });
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        // 啟動兩個線程
        th1.start();th2.start();
        // 等待兩個線程執行結束
        th1.join();th2.join();
        System.out.println(test.count);
    }

運行一下吧!你會發現永遠都可以達到我們想要的效果了~
除了上面代碼中修飾非靜態方法,還可以修飾靜態方法和代碼塊

    // 修飾靜態方法 當修飾靜態方法的時候,鎖定的是當前類的 Class 對象,即TestSynchronized2.class 。這個範圍就比對象鎖大。這裡就算是不同對象,但是只要是該類的對象,就使用的是同一把鎖。
    synchronized static void bar() {
        // 臨界區
    }
    // 修飾代碼塊 java中經典的雙重鎖檢查機制
    private volatile static TestSynchronized2 instance;
    public static TestSynchronized2 getInstance() {
        if (instance == null) {
            synchronized (TestSynchronized2.class) {
                if (instance == null) {
                    instance = new TestSynchronized2();
                }
            }
        }
        return instance;
    }

明確鎖和資源的關係

深入分析鎖定的對象和受保護資源的關係,綜合考慮受保護資源的訪問路徑,多方面考量才能用好互斥鎖。受保護資源和鎖之間的關聯關係是 N:1 的關係。如果一個資源用N個鎖,那肯定出問題的,就好像一個廁所坑位,你有10把鑰匙,那不是可以10個人同時進了?

現在給出兩段錯誤代碼,想一想到底為啥錯了吧?

    static long value1 = 0L;

    synchronized long get1() {
        return value1;
    }

    synchronized static void addOne1() {
        value1 += 1;
    }
    long value = 0L;

    long get() {
        synchronized (new Object()) {
            return value;
        }
    }

第一段錯誤原因:
因為我們說過synchronized修飾普通方法 鎖定的是當前實例對象 this 而修飾靜態方法 鎖定的是當前類的 Class 對象
所以這裡有兩把鎖 分別是 this 和 TestSynchronized3.class
由於臨界區 get() 和 addOne() 是用兩個鎖保護的,因此這兩個臨界區沒有互斥關係,臨界區 addOne() 對 value 的修改對臨界區 get() 也沒有可見性保證,這就導致併發問題了。

第二段錯誤原因:
加鎖本質就是在鎖對象的對象頭中寫入當前線程id,但是synchronized (new Object())每次在記憶體中都是新對象,所以加鎖無效。

問:剛剛的例子都是多個鎖保護一個資源,這樣百分百是不行的。那麼一個鎖保護多個資源,就一定可以了嗎?

答:如果多個資源彼此之間是沒有關聯的,那可以用一個鎖來保護。如果有關聯的話,那是不行的。比如說銀行轉賬操作,你給我轉賬,我賬戶多100,你賬戶少100,我不能用我的鎖來保護你,就像現實生活中我的鎖是不能保護你的財產的。

劃重點!要區分多個資源是否有關聯!但是一個鎖保護多個沒關聯的資源,未免性能太差了哦,比如我聽歌和玩游戲可以同時進行,你非得讓我做完一個再做另一個,豈不是要雙倍時間。所以即使一個鎖可以保護多個沒關聯的資源,但是一般而已,會各自用不同的鎖,能夠提升性能。這種鎖還有個名字,叫細粒度鎖。

問:剛剛說到銀行轉賬的案例,那麼假如某天在某銀行同時發生這樣一個事,櫃員小王需要完成A賬戶給B賬戶轉賬100元,櫃員小李需要完成B賬戶給A賬戶轉賬100元,請問如何實現呢?

答:其實用兩把鎖就實現了,轉出一把,轉入另一把。只有當兩者都成功時,才執行轉賬操作。

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(200); //A的初始賬戶餘額200
        Account b = new Account(300); //B的初始賬戶餘額200
        Thread threadA = new Thread(()->{
            try {
                transfer(a,b,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread threadB = new Thread(()->{
            try {
                transfer(b,a,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadA.start();
        threadB.start();
    }

    static void transfer(Account source,Account target, int amt) throws InterruptedException {
        synchronized (source) {
            log.info("持有鎖{} 等待鎖{}",source,target);
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

至此,恭喜你,一波問題解決了,可是遺憾的告訴你:又導致了另一個bug。這段代碼是有可能發生死鎖的!併發編程中要註意的東西可真是多喲。咱們先把死鎖這個名詞記住!持續關註【胖滾豬學編程】公眾號!在我們後面的文章中找答案!

如何保證原子性

現在我們已經知道互斥鎖可以保證原子性,也知道瞭如何使用synchronized來保證原子性。但synchronized 並不是JAVA中唯一能保證原子性的方案。

如果你粗略的看一下J.U.C(java.util.concurrent包),那麼你可以很顯眼的發現它倆:
image.png

一個是lock包,一個是atomic包,只要你英語過了四級。。我相信你都可以馬上斷定,它們可以解決原子性問題。

由於這兩個包比較重要,所以會放在後面的模塊單獨說,持續關註【胖滾豬學編程】公眾號吧!

本文轉載自公眾號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!歡迎關註!形象來源於微信表情包【胖滾家族】喜歡可以下載哦~


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

-Advertisement-
Play Games
更多相關文章
  • 1. 查看ssh伺服器是否安裝並啟動 #sudo ps -e | grep ssh 1. 安裝ssh伺服器 #sudo apt-get install openssh-server 2. 配置sshd文件 編輯SSH服務的配置文件sshd_config,修改SSH的埠和root用戶許可權。 使用到的 ...
  • 題目要求 請修改系統的root帳號密碼為redhat,確保能夠使用root帳號登錄系統。說明:server0 的IP為172.25.0.11/24desktop0的IP為172.25.0.10/24 解題過程 1 按要求打開控制台 此時用戶root是沒法登陸的 2 重啟 點擊Send key, 然後 ...
  • 【上手由易到難,推薦wsl,虛擬機】 1、tdm gcc, mingw(dev c++) 2、wsl(Windows Subsystem for Linux),簡單說就是一個能讓你在Windows上跑Linux程式的子系統 3、虛擬機 4、雙系統 5、linux單系統 6、deepin(國產,華為) ...
  • 安裝apache 啟動apache 設置apache開機自啟 訪問公網地址檢測apache是否正常 安裝MySQL資料庫 啟動MySQL資料庫 查看MySQL運行情況 查看MySQL初始密碼 登錄資料庫 修改MySQL預設密碼 創建wordpress資料庫 查看資料庫是否創建成功 退出MySQL數據 ...
  • 背景: 1.CentOS 8 已經使用 DNF 作為預設的軟體包管理器。 2.CentOS 8 中預設的軟體包倉庫已經刪除了 Docker 包,取而代之的是 Podman 和 Buildah。 大體就是 DNF 替代 YUM,Podman 替代 Docker,具體 DNF 和 Podman 的簡介這 ...
  • [TOC] 前言 CloudStack形成的基礎設施雲和 "數據中心" 運營商可以快速,輕鬆地建立在其現有的基礎設施提供雲服務的需求,彈性雲計算服務。 CloudStack用戶可以充分利用雲計算提供更高的效率,無限的規模和更快地部署新服務和系統的最終用戶。 CloudStack 是一個開源的雲操作系 ...
  • ARM64架構處理器採用48位物理定址機制,最大可以尋找到256TB的物理地址空間。對於目前的應用來說已經足夠了,不需要擴展到64位的物理地址定址。虛擬地址也同樣最大支持48位支持,所以在處理器的架構設計上,把虛擬地址空間劃分為兩個空間,每個空間最大支持256TB。Linux內核在大多數體繫結構中都 ...
  • 索引原理及B樹索引 http://hongyitong.github.io/2017/01/05/%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8AB%E6%A0%91%E7%B4%A2%E5%BC%95/ 一、索引的原理 說白了,索引問題就是一個查找問題。數 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...