深入淺出Java多線程(十):CAS

来源:https://www.cnblogs.com/CoderLvJie/p/18065516
-Advertisement-
Play Games

大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出Java多線程]系列的第十篇內容:CAS。大家覺得有用請點贊,喜歡請關註!秀才在此謝過大家了!!! ...


引言


大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出Java多線程]系列的第十篇內容:CAS。大家覺得有用請點贊,喜歡請關註!秀才在此謝過大家了!!!

在多線程編程中,對共用資源的安全訪問和同步控制是至關重要的。傳統的鎖機制,如synchronized關鍵字和ReentrantLock等,能夠有效防止多個線程同時修改同一數據導致的競態條件(race condition),但同時也帶來了一定的性能開銷。尤其是在高併發場景下,頻繁的加鎖解鎖操作可能導致線程上下文切換加劇、系統響應延遲等問題。

為了應對這一挑戰,Java從JDK 1.5版本開始引入了基於CAS(Compare And Swap)機制的原子類庫,這些原子類不僅提供了一種無鎖化的併發控制策略,還能夠在不阻塞其他線程的情況下實現高效的記憶體同步。CAS作為樂觀鎖的一種實現方式,其核心思想是在更新變數時僅當該變數的當前值與預期值相等時才會執行更新操作,否則就放棄更新並允許線程繼續嘗試或採取其他策略。

例如,在一個簡單的場景中,假設有一個被多個線程共用的整型變數i,若我們想要通過CAS將其從初始值5原子性地遞增到6,可以利用AtomicInteger類中的compareAndSet方法:

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    private static AtomicInteger sharedValue = new AtomicInteger(5);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                int oldValue = sharedValue.get();
                if (sharedValue.compareAndSet(oldValue, oldValue + 1)) {
                    System.out.println("Thread " + Thread.currentThread().getName() + " updated the value to " + (oldValue + 1));
                    break;
                }
            }
        });

        t1.start();
        // 確保t1有機會更新值
        t1.join();

        // 輸出結果應為:Thread Thread-0 updated the value to 6
    }
}

在這個示例中,如果sharedValue的當前值確實是5,那麼線程t1將成功地將它更改為6,並退出迴圈;如果有其他線程在此期間改變了sharedValue的值,則t1會不斷重試直至成功。由於CAS操作直接由CPU指令級別保證其原子性,因此不會出現因併發寫入導致的數據混亂。

通過深入探討Java多線程中的CAS技術,我們將揭示其背後的具體實現原理——Unsafe類及其native方法,剖析AtomicInteger等原子類如何藉助CAS機制實現在無鎖環境下的高效併發操作,併進一步討論在實際應用中可能出現的問題,如ABA問題、迴圈自旋消耗過大以及只能針對單個變數進行原子操作的局限性及其相應的解決方案。

在多線程編程領域中,鎖機制是實現數據同步和避免併發問題的關鍵手段。其中,樂觀鎖與悲觀鎖作為兩種不同的併發控制策略,在處理共用資源時採用了截然不同的假設和操作方式。

悲觀鎖&樂觀鎖


悲觀鎖

悲觀鎖,顧名思義,採取保守的策略對待併發訪問。它假定每次對共用資源進行操作時都可能發生衝突,因此在執行任何更新前都會預先鎖定資源。例如,在Java中使用synchronized關鍵字或ReentrantLock等工具實現悲觀鎖時,一個線程在獲取鎖後才能進入臨界區執行代碼,其他線程則必須等待鎖釋放後才能獲得執行機會。以下是一個簡單的悲觀鎖示例:

public class PessimisticLockExample {
    private final Lock lock = new ReentrantLock();

    public void decrementCounter() {
        lock.lock(); // 獲取悲觀鎖
        try {
            // 臨界區代碼
            int count = this.count;
            if (count > 0) {
                this.count--;
            }
        } finally {
            lock.unlock(); // 釋放悲觀鎖
        }
    }

    // 共用資源變數
    private int count = 10;
}

在這個例子中,當一個線程試圖修改計數器時,會先鎖定整個方法,確保同一時間只有一個線程能夠執行減一操作。這種機制雖然保證了數據一致性,但可能造成線程間的頻繁阻塞和上下文切換,尤其在高併發環境下性能損耗明顯。

樂觀鎖

相對而言,樂觀鎖則是基於積極樂觀的假設:認為大部分情況下多個線程同時訪問同一資源並不會發生衝突。因此,樂觀鎖允許線程無須獲取鎖就可以執行業務邏輯,僅在更新數據時採用CAS(Compare And Swap)原子操作檢查並更新數據。如果發現數據已被其它線程改變,則放棄本次更新,通常會重新讀取數據並再次嘗試。

以Java中的AtomicInteger為例,它利用CAS機制實現了樂觀鎖的特性:

public class OptimisticLockExample {
    private final AtomicInteger counter = new AtomicInteger(10);

    public void incrementCounter() {
        while (true) { // 自旋
            int currentValue = counter.get();
            int newValue = currentValue + 1;
            if (counter.compareAndSet(currentValue, newValue)) { // 使用CAS原子操作
                break// 更新成功,退出迴圈
            }
        }
    }
}

// AtomicInteger 的 compareAndSet 方法源碼簡化示意
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

上述代碼展示瞭如何在一個迴圈內連續嘗試原子地增加計數器值。只有噹噹前值等於預期值時,CAS操作才會成功,否則線程將不斷重試直至成功更新。由於樂觀鎖在沒有衝突的情況下不涉及線程掛起,故適用於“讀多寫少”的場景,能有效降低加鎖開銷,提高系統吞吐量。然而,若併發更新頻率較高,可能會導致大量的CAS失敗和重試,從而帶來額外的CPU消耗。

CAS原理


在併發編程中,CAS(Compare and Swap,比較並交換)是一種無鎖演算法,它在不阻塞其他線程的情況下實現原子性的變數更新操作。在Java中,CAS的實現基於Unsafe類提供的native方法,這些方法直接與底層硬體交互,利用CPU級別的原子指令來保證數據更新的安全性。

CAS流程

在CAS操作中涉及三個關鍵值:V(要更新的變數),E(預期值),N(新值)。當需要對一個共用變數進行修改時,線程首先檢查該變數當前值是否等於預期值E。如果相等,則將變數值更新為新值N;如果不等,則說明已經有其他線程更新了該變數,此時當前線程放棄更新操作,保持原值不變。

以AtomicInteger為例,我們可以通過以下代碼片段理解CAS的工作過程:

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest {
    private AtomicInteger counter = new AtomicInteger(5);

    public void increment() {
        int expectedValue = counter.get();
        while (!counter.compareAndSet(expectedValue, expectedValue + 1)) {
            // 當前線程獲取到的值已經被其他線程改變,重新獲取最新值
            expectedValue = counter.get();
        }
    }
}

在這個例子中,compareAndSet方法會不斷嘗試將計數器從舊值遞增1,直到成功為止。當多個線程同時嘗試增加計數器時,只有一個線程能夠通過CAS成功更新,其餘線程將繼續迴圈直至其看到的預期值和實際值匹配後再嘗試更新。

原子性和操作系統

CAS的核心優勢在於其原子性——即整個比較和交換的操作作為一個不可分割的整體執行。在現代多核CPU架構下,諸如cmpxchg指令這樣的原子指令能夠確保在沒有外部干預的情況下完成這一系列步驟。在Linux X86系統中,cmpxchgl指令配合lock首碼可以確保在同一時刻僅有一個處理器能成功更新記憶體位置,從而避免了併發問題。

ABA問題

儘管CAS機制在大多數情況下表現優異,但存在一種特殊情況——ABA問題。假設一個變數初始值為A,被更改為B後又改回A,這種情況下使用單純的CAS檢查將會誤判為未發生過變化。為了應對ABA問題,JDK提供了一個名為AtomicStampedReference的類,它在每個對象引用上附加了一個版本號或時間戳,使得每次更改不僅檢查引用本身,還檢查版本號,只有兩者都匹配時才會進行替換。

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABATest {
    private AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10);

    public void update(int newValue, int newStamp) {
        while (true) {
            int currentStamp = ref.getStamp();
            if (ref.compareAndSet(1, newValue, currentStamp, newStamp)) {
                break// 更新成功
            } else {
                // 失敗則重試,獲取最新的stamp
            }
        }
    }
}

在上述代碼中,compareAndSet方法不僅要比較引用對象的值,還要比較並更新相關聯的版本信息,因此有效防止了ABA問題的發生。

綜上所述,CAS作為一種高效的無鎖同步機制,在Java多線程編程中扮演著重要角色,通過直接調用CPU指令實現了併發環境下的原子操作,但也需要註意潛在的ABA問題以及長時間自旋帶來的性能開銷等問題,並選擇合適的解決方案。

Unsafe類


在Java中,為了能夠直接與底層硬體進行交互並執行原子操作,如CAS,Java使用了一個名為sun.misc.Unsafe的類。由於該類提供了一些不受JVM訪問控制約束的方法,並允許開發者直接操作記憶體和執行非安全但高效的原語操作,因此被稱為“Unsafe”。儘管這個類不在公共API中,但在併發包java.util.concurrent.atomic中的原子類,如AtomicInteger等,都依賴於Unsafe類提供的CAS操作來保證線程間的原子性和可見性。

Unsafe類與CAS方法 Unsafe類包含了一系列native方法,這些方法用於執行原子性的CAS操作,例如:

public native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

這些方法分別用於比較並交換對象引用、整型值以及長整型值。參數含義如下:

  • o:一個對象實例,CAS操作將作用在其內部的一個欄位上。
  • offset:指定欄位相對於對象起始地址的偏移量,由objectFieldOffset()方法計算得出。
  • expected:期望的舊值,只有當欄位當前值等於此預期值時,才會進行更新。
  • x:新值,如果條件滿足,則用新值替換舊值。

以AtomicInteger為例,其getAndAddInt方法就利用了Unsafe類的compareAndSwapInt方法實現原子遞增:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset); // 獲取當前值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS嘗試更新
    return v; // 返回更新前的值
}

這裡首先獲取到共用變數的當前值v,然後在一個迴圈中不斷嘗試通過CAS指令將變數從v更新為v+delta,直到成功為止。

CPU級別的原子操作 值得註意的是,CAS操作在Java中的實現實際上調用了操作系統和CPU提供的原子指令。在Linux X86系統下,是通過cmpxchgl這樣的CPU指令實現的,而在多處理器環境中,為了確保跨多個CPU核心的原子性,還需要配合lock首碼指令鎖定匯流排或緩存行,防止其他處理器同時修改同一數據。

弱版本CAS與強版本CAS的區別 從JDK 9開始,Unsafe類提供了兩個看似相似但實際上可能有不同實現策略的方法:compareAndSetIntweakCompareAndSetInt。雖然在早期版本中它們的行為一致,但在某些情況下,weakCompareAndSet系列方法可能只保留了volatile變數本身的特性,而放棄了happens-before規則帶來的記憶體語義保障。這意味著weakCompareAndSet無法確保除了目標volatile變數以外的其他變數的操作順序和可見性,從而有可能帶來更高的性能,但也可能需要開發人員更小心地處理併發邏輯。

總之,Java通過Unsafe類實現了對CAS原子操作的支持,使得程式員可以在高級語言層面上利用底層硬體的原子指令,構建出高效且無鎖化的併發程式。然而,這也要求開發者具備對併發編程機制深刻的理解,以便正確解決潛在的問題,比如ABA問題,以及合理應對CAS自旋可能導致的性能開銷。

AtomicInteger源碼簡析


Java併發包中的java.util.concurrent.atomic.AtomicInteger類是一個基於CAS實現的線程安全整數容器,它提供了一系列原子操作方法,如get、set、incrementAndGet等。以getAndAdd(int delta)方法為例,該方法用於獲取當前值並原子性地將值增加指定的delta。

Java 17下的Atomic類:

首先,我們觀察到getAndAdd(int delta)方法調用了Unsafe類的getAndAddInt()方法:

public final int getAndAdd(int delta) {
    return U.getAndAddInt(this, VALUE, delta);
}

這裡的UUnsafe類的一個實例,其內部欄位VALUE存儲了AtomicInteger類中value變數相對於對象起始地址的偏移量。objectFieldOffset()方法用於計算這個偏移量:

private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

然後,深入到Unsafe類的getAndAddInt()方法實現:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset); // 獲取volatile類型的舊值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS更新新值
    return v; // 返回更新前的值
}

這段代碼展示了典型的CAS迴圈模式。首先通過getIntVolatile()讀取記憶體中AtomicInteger實例的volatile變數value的當前值,並保存在局部變數v中。接下來進入一個do-while迴圈,在迴圈體內嘗試使用weakCompareAndSetInt()執行CAS操作。只有當value的當前值等於我們剛讀取到的v時,才會將value設置為v+delta。如果此時

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

-Advertisement-
Play Games
更多相關文章
  • JDBC是指資料庫連接技術,用於java連接mySQL等資料庫。本文詳細介紹了尚矽谷課程中JDBC的學習內容和補充知識。 概述 java語言只提供規範介面,存在於java.sql.javax.sql包下,然後資料庫軟體根據java提供的規範實現具體的驅動代碼(jar) jar包是java程式打成的一 ...
  • 不知道大家在 Java 日常開發中是否會經常遇到關於 JSON 的各種轉換的場景,我把自己日常工作中遇到的 JSON轉換場景做了一個總結,希望可以對大家有幫助。 註:本文都是基於阿裡的 fastjson 來講解操作的。 ...
  • Redis大家都不陌生,就算是沒用過,也都聽說過了。 作為最廣泛使用的KV記憶體資料庫之一,在當今的大流量時代,單機模式略顯單薄,免不了要有一些拓展的方案。 筆者下文會對各種方案進行介紹,並且給出場景,實現 等等概述,還會提到一些新手常見的誤區。 正文 先從基礎的拓展方式開始,這樣更便於理解較高級的模 ...
  • 系統選擇 目前市面上主流的桌面操作系統在大多數人眼裡只有Windows和MacOS,那為什麼我沒選擇它們兩呢? 首先,不選MacOS的原因,就是太貴。當然這是我的原因不是蘋果的原因,我最早使用Linux寫代碼的時候是2018年,那時候剛畢業上班不久,根本買不起Mac(雖然現在也覺得有點貴)。 在沒有 ...
  • 在這篇全面解析CDN的技術文章中,我們深入探討了CDN的基礎概念、核心架構、多樣化產品和在不同行業中的應用案例。文章揭示了CDN技術如何優化內容分發,提升用戶體驗,並展望了CDN面臨的挑戰和未來發展趨勢。 關註【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互 ...
  • 寫在前面 在Java日常開發過程中,實現Excel文件的導入導出功能是一項常見的需求。 通過使用相關的Java庫,如Apache POI、EasyPoi或EasyExcel,可以輕鬆地實現Excel文件的讀寫操作。 而這篇文章將介紹如何在Java中使用Apache POI、EasyPoi 和Easy ...
  • 學了分塊,感覺這玩意好難啊,怎麼聽起來這麼簡單?【】【】分塊! 先推薦一個東西:loj 分塊全家桶! 首先,把一整個數組劈成 \(\sqrt n\) 塊是最優的!(當然如果你想寫一個 \(114514\) 塊的分塊也沒問題但他不優啊!) 長這樣: 這樣它的複雜度是: 預處理:\(O(n\sqrt n ...
  • 前言:配合狂神老師的教學視頻使用效果更佳: https://www.bilibili.com/video/BV1NE411Q7Nx/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...