欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java中多線程的ABA場景問題分析

 更新時間:2022年12月29日 16:51:02   作者:Chavin  
這篇文章主要為大家介紹了Java中多線程的ABA場景問題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

本文是筆者在日常開發(fā)過程中遇到的對 CAS 、 ABA 問題以及 JUC(java.util.concurrent)中 AtomicReference 相關(guān)類的設(shè)計的一些思考記錄。 對需要處理 ABA 問題,或有諸如筆者一樣的設(shè)計疑問探索好奇心的讀者可能會帶來一些啟發(fā)。

本文主體由三部分構(gòu)成:

  • 首先闡述多線程場景數(shù)據(jù)同步的常用語言工具
  • 接著闡述什么是 ABA 問題,以及產(chǎn)生的原因和可能帶來的影響
  • 再探索 JUC 中官方為解決 ABA 問題而做一些工具類設(shè)計

文章的最后會對多線程數(shù)據(jù)同步常用解決方案做了簡短地經(jīng)驗性總結(jié)與概括。

受限于筆者的理解與知識水平,文章的一些術(shù)語表述難免可能會失偏頗,對于有理解歧義或爭議的部分,歡迎大家探討和指正。

一、異步場景常用工具

在Java中的多線程數(shù)據(jù)同步的場景,常會出現(xiàn):

  • 關(guān)鍵字 volatile
  • 關(guān)鍵字 synchronized
  • 可重入鎖/讀寫鎖 java.util.concurrent.locks.*
  • 容器同步包裝,如 Collections.synchronizedXxx()
  • 新的線程安全容器,如 CopyOnWriteArrayList/ConcurrentHashMap
  • 阻塞隊列 java.util.concurrent.BlockingQueue
  • 原子類 java.util.concurrent.atomic.*
  • 以及 JUC 中其他工具諸如 CountDownLatch/Exchanger/FutureTask 等角色。

其中 volatile 關(guān)鍵字用于刷新數(shù)據(jù)緩存,即保證在 A 線程修改某數(shù)據(jù)后,B 線程中可見,這里面涉及的線程緩存和指令重排因篇幅原因不在本文探討范圍之內(nèi)。而不論是 synchronized 關(guān)鍵字下的對象鎖,還是基于同步器 AbstractQueuedSynchronizerLock 實現(xiàn)者們,它們都屬于悲觀鎖。而在同步容器包裝、新的線程程安全容器和阻塞隊列中都使用的是悲觀鎖;只是各類的內(nèi)部使用不同的 Lock 實現(xiàn)類和 JUC 工具,另外不同容器在加鎖粒度和加鎖策略上分別做了處理和優(yōu)化。

這里值得一說的,也是本文聚焦的重點則是原子類,即 java.util.concurrent.atomic.* 包下的幾個類庫諸如 AtomicBoolean/AtomicInteger/AtomicReference

二、CAS 與 ABA 問題

我們知道在使用悲觀鎖的場景中,如果有有一個線程搶先取得了鎖,那么其他想要獲得鎖的線程就得被阻塞等待,直到占鎖線程完成計算釋放鎖資源。而現(xiàn)代 CPU 提供了硬件級指令來實現(xiàn)同步原語,也就是說可以讓線程在運行過程中檢測是否有其他線程也在對同一塊內(nèi)存進行讀寫,基于此 Java 提供了使用忙循環(huán)來取代阻塞的系列工具類 AutomicXxx,這屬于是一種樂觀鎖的實現(xiàn)。其常規(guī)使用方式形如:

public class Requester {
    private AtomicBoolean isRequesting = new AtomicBoolean(false)
    public void request() {
        // 修改成功時返回true;compareAndSet 方法由 Native 層調(diào)硬件指令實現(xiàn)
        if (!isRequesting.compareAndSet(false, true)) {
            return;
        }
        try {
            // do sth...
        } finally {
            isRequesting.set(false)
        }
    }
}

進入到 JDK11 AtomicBoolean 的源碼中,可以看到 compareAndSet 最終調(diào)用 Native 層的方式如下。其實在舊的版本中 JDK 是使用 Unsafe 類處理的,在入?yún)?shù)中有傳入狀態(tài)變量的字段偏移值,新版本則將兩者封裝到 VarHandle 中采用DL方式查找依賴(筆者猜測可能和JDK9模塊化改造有關(guān)):

// 舊版
public class AtomicBoolean {
    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private static final long VALUE;
    static {
        try {
            VALUE = U.objectFieldOffset
                (AtomicBoolean.class.getDeclaredField("value"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }
    private volatile int value;
    public final boolean compareAndSet(boolean expect, boolean update) {
        return U.compareAndSwapInt(this, VALUE, (expect ? 1 : 0), (update ? 1 : 0));
    }
}
// 新版
public class AtomicBoolean {
    private static final VarHandle VALUE;
    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicBoolean.class, "value", int.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }
    private volatile int value;
    public final boolean compareAndSet(boolean expectedValue, boolean newValue) {
        return VALUE.compareAndSet(this, (expectedValue ? 1 : 0), (newValue ? 1 : 0));
    }
}

猶如入倉有 thisvalue 的偏移值,則 Native 層可根據(jù)此二者值定位到某塊棧內(nèi)存,這樣對于基本類型沒什么問題。原子類型體系中使用 AtomicReference 來引用復(fù)合類型實例,但 Java 中 Object 類型在棧中保存的只是堆中對象數(shù)據(jù)塊的地址,其結(jié)構(gòu)形如下圖:

而實際運行過程中,調(diào)用 AtomicReference#compareAndSet() 時,Native層只會對比棧中內(nèi)存的值,而不會關(guān)注其指向的堆中數(shù)據(jù)。這樣說可能有點抽象,看一段實驗代碼:

StringBuilder varA = new StringBuilder("abc");
StringBuilder varB = new StringBuilder("123");
AtomicReference<StringBuilder> ref = new AtomicReference<>(varA);
ref.compareAndSet(varA, varB); // (1)
System.out.println(ref.get()); // (2) varB->123
varB.append('4'); // (3) changed varB->1234
if (ref.compareAndSet(varB, varA)) { // (4)
    System.out.println("CAS succeed"); // (5) CAS succeed
}
System.out.println(ref.get()); // abc

喜歡動手的讀者可以嘗試自定義一個類,觀察下 Compare 過程是否真的沒有調(diào)用對象的 equals 方法。

ref 在經(jīng)過處理后再 (2) 處引用變量B,而在注釋 (3) 處將 B 值修改了,但由于原子類不會檢查堆中數(shù)據(jù),所以還是能通過注釋 (4) 處的相等比較走到注釋 (5) 。這也就引入了 所謂的 ABA 問題:

  • 假設(shè),線程 1 的任務(wù)希望將變量從 A 變?yōu)?C ,但執(zhí)行到一半被線程 2 搶走 CPU
  • 線程 2 將變量從 A 改成了 B ,此時 CPU 時間片又被系統(tǒng)分給了線程 3
  • 線程 3 講變量從 B 又設(shè)置成一個新的 A 。
  • 線程 1 獲取時間片,檢查變量發(fā)現(xiàn)其仍然是 A(但 A 對象內(nèi)部的數(shù)據(jù)已經(jīng)改變了),檢查通過將變量置為 C 。

若業(yè)務(wù)場景中,線程 1 不在意變量經(jīng)過了一輪變化,也不在意 A 中數(shù)據(jù)是否有變化,則該問題無關(guān)痛癢。而若線程 1 對這兩個變化敏感,則將變量置為 C 的操作就不符合預(yù)期了。用維基百科的例子來表述,其大意是:

你提著有很多現(xiàn)金的包去機場,這時來了個辣妹挑 逗你,并趁你不注意時用一個看起來一樣的空包換了你的現(xiàn)金包,然后她就走了;此時你檢查了下發(fā)現(xiàn)你的包還在,于是就匆忙拿著包趕飛機去了。

換個角度看這幾個關(guān)鍵字:

  • 有現(xiàn)金的包:指向堆中數(shù)據(jù)的棧引用
  • 辣妹挑 逗:其他線程搶占 CPU
  • 看起來一樣空包:其他線程修改堆中數(shù)據(jù)
  • 發(fā)現(xiàn)包還在:僅檢查棧中內(nèi)存的地址值是否一致

三、用 JUC 工具處理 ABA 問題

為處理 ABA 問題,JDK 提供了另外兩個工具類:AtomicMarkableReferenceAtomicStampedReference 他們除了對比棧中對象的引用地址外,另外還保存了一個 booleanint 類型的標(biāo)記值,用于 CAS 比較。

StringBuilder varA = new StringBuilder("abc");
StringBuilder varB = new StringBuilder("123");
AtomicStampedReference<StringBuilder> ref = new AtomicStampedReference<>(varA, varA.toString().hashCode());
ref.compareAndSet(varA, varB, varA.toString().hashCode(), varB.toString().hashCode());
System.out.println(ref.get(new int[1]));
varB.append('4');
// CAS失敗,因為Stamp值對不上
if (ref.compareAndSet(varB, varA, varB.toString().hashCode(), varA.toString().hashCode())) {
    System.out.println("compareAndSet: succeed");
}
System.out.println(ref.get(new int[1]));

注:這種設(shè)計和為快速判斷文件是否相同,而比較文件摘要值(MD5、SHA值)和預(yù)期是否一致的思想倒有異曲同工之妙。

總結(jié)

通常在多線程場景中,這些工具的應(yīng)用場景具有各自的適用特征:

  • 若各線程讀寫數(shù)據(jù)沒有競爭關(guān)系,則可考慮僅使用 volatile 關(guān)鍵字;
  • 若各線程對某數(shù)據(jù)的讀寫需要去重,則可優(yōu)先考慮使用樂觀鎖實現(xiàn),即用原子類型;
  • 若各線程有競爭關(guān)系且不去重必須按順序搶占某資源,即必須用鎖阻塞,若沒有多條件隊列的訴求則可先考慮使用 synchronized 添加對象鎖(但需注意鎖對象的不可變和私有化),否則考慮用 Lock 實現(xiàn)類,但特別的如需讀寫分鎖以實現(xiàn)共享鎖則只能用 Lock 了。
  • 若需使用線程安全容器,出于性能考慮優(yōu)先考慮 java.util.concurrent.* 類,如 ConcurrentHashMapCopyOnWriteArrayList;再考慮使用容器同步包裝 Collections.synchronizedXxx()。而阻塞隊列則多用于生產(chǎn)-消費模型中的任務(wù)容器,典型如用在線程池中。

以上就是Java中多線程的ABA場景問題分析的詳細(xì)內(nèi)容,更多關(guān)于Java 多線程ABA分析的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • springboot整合Mybatis、JPA、Redis的示例代碼

    springboot整合Mybatis、JPA、Redis的示例代碼

    這篇文章主要介紹了springboot整合Mybatis、JPA、Redis的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-09-09
  • 基于springmvc之常用注解,操作傳入?yún)?shù)

    基于springmvc之常用注解,操作傳入?yún)?shù)

    這篇文章主要介紹了springmvc之常用注解,操作傳入?yún)?shù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • JavaWeb實戰(zhàn)之開發(fā)網(wǎng)上購物系統(tǒng)(超詳細(xì))

    JavaWeb實戰(zhàn)之開發(fā)網(wǎng)上購物系統(tǒng)(超詳細(xì))

    這篇文章主要介紹了JavaWeb實戰(zhàn)之開發(fā)網(wǎng)上購物系統(tǒng)(超詳細(xì)),文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java的小伙伴們有很好的幫助,需要的朋友可以參考下
    2021-04-04
  • spring配置不掃描service層的原因解答

    spring配置不掃描service層的原因解答

    這篇文章主要介紹了spring配置不掃描service層的原因解答,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • Java 獲取原始請求域名實現(xiàn)示例

    Java 獲取原始請求域名實現(xiàn)示例

    這篇文章主要為大家介紹了Java 獲取原始請求域名實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-12-12
  • SpringMVC中的DispatcherServlet請求分析

    SpringMVC中的DispatcherServlet請求分析

    這篇文章主要介紹了SpringMVC中的DispatcherServlet請求分析, DispatcherServlet作為一個Servlet,那么當(dāng)有請求到Tomcat等Servlet服務(wù)器時,會調(diào)用其service方法,再調(diào)用到其父類GenericServlet的service方法,需要的朋友可以參考下
    2024-01-01
  • java FastJson的簡單用法

    java FastJson的簡單用法

    FastJson是阿里的開源JSON解析庫,可以解析JSON格式的字符串,支持將Java Bean序列化為JSON字符串,也可以從JSON字符串反序列化到JavaBean,這里我介紹一下FastJson的使用,感興趣的朋友一起看看吧
    2021-09-09
  • 淺談Spring Boot 異常處理篇

    淺談Spring Boot 異常處理篇

    本篇文章主要介紹了淺談Spring Boot 異常處理篇,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • hutool?工具類基本使用教程

    hutool?工具類基本使用教程

    Hutool?是一個?Java?工具包,也只是一個工具包,它幫助我們簡化每一行代碼,減少每一個方法,讓?Java?語言也可以?“甜甜的”,下面通過本文學(xué)習(xí)下hutool?工具類基本使用教程,感興趣的朋友跟隨小編一起看看吧
    2021-12-12
  • Spring Boot 使用 Swagger 構(gòu)建 RestAPI 接口文檔

    Spring Boot 使用 Swagger 構(gòu)建 RestAPI 接口文檔

    這篇文章主要介紹了Spring Boot 使用 Swagger 構(gòu)建 RestAPI 接口文檔,幫助大家更好的理解和使用Spring Boot框架,感興趣的朋友可以了解下
    2020-10-10

最新評論