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

Java中多線程的ABA場(chǎng)景問題分析

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

前言

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

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

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

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

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

一、異步場(chǎng)景常用工具

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

  • 關(guān)鍵字 volatile
  • 關(guān)鍵字 synchronized
  • 可重入鎖/讀寫鎖 java.util.concurrent.locks.*
  • 容器同步包裝,如 Collections.synchronizedXxx()
  • 新的線程安全容器,如 CopyOnWriteArrayList/ConcurrentHashMap
  • 阻塞隊(duì)列 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)鍵字下的對(duì)象鎖,還是基于同步器 AbstractQueuedSynchronizerLock 實(shí)現(xiàn)者們,它們都屬于悲觀鎖。而在同步容器包裝、新的線程程安全容器和阻塞隊(duì)列中都使用的是悲觀鎖;只是各類的內(nèi)部使用不同的 Lock 實(shí)現(xiàn)類和 JUC 工具,另外不同容器在加鎖粒度和加鎖策略上分別做了處理和優(yōu)化。

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

二、CAS 與 ABA 問題

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

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

進(jìn)入到 JDK11 AtomicBoolean 的源碼中,可以看到 compareAndSet 最終調(diào)用 Native 層的方式如下。其實(shí)在舊的版本中 JDK 是使用 Unsafe 類處理的,在入?yún)?shù)中有傳入狀態(tài)變量的字段偏移值,新版本則將兩者封裝到 VarHandle 中采用DL方式查找依賴(筆者猜測(cè)可能和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)存,這樣對(duì)于基本類型沒什么問題。原子類型體系中使用 AtomicReference 來引用復(fù)合類型實(shí)例,但 Java 中 Object 類型在棧中保存的只是堆中對(duì)象數(shù)據(jù)塊的地址,其結(jié)構(gòu)形如下圖:

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

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

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

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

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

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

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

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

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

三、用 JUC 工具處理 ABA 問題

為處理 ABA 問題,JDK 提供了另外兩個(gè)工具類:AtomicMarkableReferenceAtomicStampedReference 他們除了對(duì)比棧中對(duì)象的引用地址外,另外還保存了一個(gè) 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失敗,因?yàn)镾tamp值對(duì)不上
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è)計(jì)和為快速判斷文件是否相同,而比較文件摘要值(MD5、SHA值)和預(yù)期是否一致的思想倒有異曲同工之妙。

總結(jié)

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

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

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

相關(guān)文章

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

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

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

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

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

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

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

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

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

    Java 獲取原始請(qǐng)求域名實(shí)現(xiàn)示例

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

    SpringMVC中的DispatcherServlet請(qǐng)求分析

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

    java FastJson的簡(jiǎn)單用法

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

    淺談Spring Boot 異常處理篇

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

    hutool?工具類基本使用教程

    Hutool?是一個(gè)?Java?工具包,也只是一個(gè)工具包,它幫助我們簡(jiǎn)化每一行代碼,減少每一個(gè)方法,讓?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

最新評(píng)論