0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

在多線程的情況下如何對(duì)一個(gè)值進(jìn)行 a++ 操作

科技綠洲 ? 來源:Java技術(shù)指北 ? 作者:Java技術(shù)指北 ? 2023-10-13 11:17 ? 次閱讀

在多線程的情況下,對(duì)一個(gè)值進(jìn)行 a++ 操作,會(huì)出現(xiàn)什么問題?

a++ 的問題

先寫個(gè) demo 的例子。把 a++ 放入多線程中運(yùn)行一下。定義 10 個(gè)線程,每個(gè)線程里面都調(diào)用 5 次 a++,把 a 用 volatile 修飾,可以讓 a 的值在修改之后,所有的線程立刻就可以知道。最后結(jié)果是不是 50,還是其他的數(shù)字?

public class Test {

    private static volatile  int a = 0;

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable(){

                @Override
                public void run() {
                   try {
                        for(int j = 0; j < 10; j++) {
                            System.out.print(a++ + ", ");
                            Thread.sleep(100);
                        }
                    } catch (Exception e) {

                    }
                }
            });
            threads[i].start();
        }
    }
}

圖片

從結(jié)果上看 a++ 的操作并沒有達(dá)到預(yù)期值的 50,而是少了很多,其中還有一定是有問題的。那就是因?yàn)?a++ 的操作并不是原子性的。

原子性

并發(fā)編程,有三大原則:有序性、可見性、原子性

  1. 有序性:正常編譯器執(zhí)行代碼,是按順序執(zhí)行的。有時(shí)候,在代碼順序?qū)Τ绦虻慕Y(jié)果沒有影響時(shí),編譯器可能會(huì)為了性能從而改變代碼的順序。
  2. 可見性:一個(gè)線程修改了一個(gè)變量的值,另外一個(gè)線程立刻可以知道修改后的值。
  3. 原子性:一個(gè)操作或者多個(gè)操作在執(zhí)行的時(shí)候,要么全部被執(zhí)行,要么全部都不執(zhí)行。

上面的 a++ 就沒有原子性,它有三個(gè)步驟:

  1. 在內(nèi)存中讀取了 a 的值。
  2. 對(duì) a 進(jìn)行了 + 1 操作。
  3. 將新的 a 值刷回到內(nèi)存。

這三個(gè)步驟可以被示例中的 10 個(gè)線程上下文切換打斷:當(dāng) a = 10

  1. 線程 1 將 a 的值讀取到內(nèi)存, a = 10
  2. 線程 2 將 a 的值讀取到內(nèi)存, a = 10
  3. 線程 1 將 a + 1,a = 11
  4. 此時(shí)線程發(fā)生切換,線程 2 對(duì) a 進(jìn)行 + 1 操作, a = 11
  5. 線程 2 將 a 的值寫回到內(nèi)存, a = 11
  6. 線程 1 將 a 的值寫回到內(nèi)存, a = 11

從上面的步驟中可以看出 a 的值在兩次相加后沒有得到 12 的值,而是 11。這就是 a++ 引發(fā)的問題。

小 B 把上面的步驟對(duì)面試官講了一遍,面試官又問了,有什么方式可以避免這個(gè)問題,小 B 不加思索的回答用 synchronized 加鎖。面試官說 synchronized 太重了,還有其他的解決方式嗎?小 B 暈了。其實(shí)可以使用 AtomicInteger 的 incrementAndGet() 方法。

AtomicInteger 源碼分析

主要屬性

首先看看 AtomicInteger 的主要屬性。

//sun.misc 下的類,提供了一些底層的方法,用于和操作系統(tǒng)交互
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value 字段的內(nèi)存地址相對(duì)于對(duì)象內(nèi)存地址的偏移量
private static final long valueOffset;
//通過 unsafe 初始化 valueOffset,獲取偏移量
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

// 用 valatile 修飾的值,保證了內(nèi)存的可見性
private volatile int value;

從屬性中可以看出 AtomicInteger 調(diào)用的是 Unsafe 類,Unsafe 類中大多數(shù)的方法是用 native 修飾的,可以直接進(jìn)行一些系統(tǒng)級(jí)別的操作。

用 volatile 修飾 value 值,保證了一個(gè)線程的值對(duì)另外一個(gè)線程立即可見。

incrementAndGet()

//AtomicInteger.incrementAndGet()
public final int incrementAndGet() {
    //調(diào)用 unsafe.getAndAddInt()
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

//Unsafe.getAndAddInt()
//參數(shù):需要操作的對(duì)象,偏移量,要增加的值
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;
}

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

incrementAndGet() 首先獲取了當(dāng)前值,然后調(diào)用 compareAndSwapInt() 方法更新數(shù)據(jù)。

compareAndSwapInt() 是 CAS 的縮寫來源,比較并替換。被 native 修飾,調(diào)用了操作系統(tǒng)底層的方法,保證了硬件級(jí)別的原子性。

var2,var4,var5 是它的三個(gè)操作數(shù),表示內(nèi)存地址偏移量 valueOffset,預(yù)期原值 expect,新的值 update。把 this.compareAndSwapInt(var1, var2, var5, var5 + var4) 變成 this.compareAndSwapInt(obj, valueOffset, expect, update),釋義就是如果內(nèi)存位置中的 valueOffset 值 與 expect 的值相同,就把內(nèi)存中的 valueOffset 改成 update,否則不操作。

getAndAddInt() 方法中用了 do-while,就相當(dāng)于如果 CAS 一直更新不成功,就不退出循環(huán)。直到更新成功為止。

ABA 問題

CAS 操作也并不是沒有問題的。

  1. 循環(huán)操作時(shí)間長(zhǎng)了,開銷大。用了 do-while,如果更新一直不成功,就一直在循環(huán)。會(huì)給 CPU 帶來很大的開銷。
  2. 只能保證一個(gè)共享變量的原子性。循環(huán) CAS 的方式只能保證一個(gè)變量進(jìn)行原子操作,在對(duì)多個(gè)變量進(jìn)行 CAS 的時(shí)候就沒辦法保證原子性了。
  3. ABA 問題。CAS 的操作一般是 1. 讀取內(nèi)存偏移量 valueOffset。2. 比較 valueOffset 和 expect 的值。3. 更新 valueOffset 的值。如果線程 A 讀取 valueOffset 后,線程 B 修改了 valueOffset 的值,并且將 valueOffset 的值又改了回來。線程 A 會(huì)認(rèn)為 valueOffset 的值并沒有改變。這就是 ABA 問題。要解決這個(gè)問題,就是在每次修改 valueOffset 值的時(shí)候帶上一個(gè)版本號(hào)。

總結(jié)

這篇文章介紹了 CAS,它是 java 中的樂觀鎖,每次認(rèn)為操作并不會(huì)有其他線程去修改數(shù)據(jù),如果有其他線程操作了數(shù)據(jù),就重試,一直到成功為止。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 編程
    +關(guān)注

    關(guān)注

    88

    文章

    3565

    瀏覽量

    93536
  • 多線程
    +關(guān)注

    關(guān)注

    0

    文章

    277

    瀏覽量

    19897
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4722

    瀏覽量

    68231
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Java多線程的用法

    本文將介紹一下Java多線程的用法。 基礎(chǔ)介紹 什么是多線程 指的是個(gè)進(jìn)程中同時(shí)運(yùn)行多個(gè)
    的頭像 發(fā)表于 09-30 17:07 ?916次閱讀

    多線程編程之: 問題提出

    多線程編程之 問題提出編寫個(gè)耗時(shí)的單線程程序:  新建
    發(fā)表于 10-22 11:41

    LabView的多線程語言

    LabView的多線程語言以前只會(huì)照貓畫虎的寫些簡(jiǎn)單的程序,些基本原理不是很清晰。從網(wǎng)上找了些資料,這里總結(jié)一下。1。
    發(fā)表于 06-08 10:13

    基于51單片機(jī)的多線程操作系統(tǒng) 精選資料分享

    我知道,51單片機(jī)上運(yùn)行個(gè)操作系統(tǒng),大多數(shù)情況下并不實(shí)用。但51單片機(jī)廣為人知。所以我認(rèn)為,用它來逐步的實(shí)現(xiàn)
    發(fā)表于 07-20 07:55

    如何使用多線程和異步操作等并發(fā)設(shè)計(jì)方法來最大化程序的性能

      因?yàn)楫惒?b class='flag-5'>操作無須額外的線程負(fù)擔(dān),并且使用回調(diào)的方式進(jìn)行處理,設(shè)計(jì)良好的情況下,處理函數(shù)可以不必使用共享變量(即使無法完全不用,最起碼可
    發(fā)表于 08-23 16:31

    MCU開發(fā)中使用多線程操作讀是否需要保護(hù)?

    ,那么多線程訪問是安全的,那么對(duì)于讀,某些情況下需要保護(hù),某些情況下其實(shí)可以不需要保護(hù)。
    發(fā)表于 02-01 15:42

    很多變量多線程讀寫是使用關(guān)中斷好還是使用互斥進(jìn)行保護(hù)呢?

    我想問一下,就是我有很多變量會(huì)多線程讀寫操作,有些會(huì)比較頻繁,我讀寫的時(shí)候是使用中斷去保護(hù)還是增加互斥量去保護(hù)。 1.如果加互斥量,當(dāng)前低優(yōu)先級(jí)讀寫
    發(fā)表于 05-05 14:14

    QNX環(huán)境多線程編程

    介紹了QNX 實(shí)時(shí)操作系統(tǒng)和多線程編程技術(shù),包括線程間同步的方法、多線程程序的分析步驟、線程基本程序結(jié)構(gòu)以及實(shí)用編譯方法。QNX 是由加拿大
    發(fā)表于 08-12 17:37 ?30次下載

    基于多線程環(huán)境的遞增操作--原子操作

    因此多線程環(huán)境中對(duì)個(gè)變量進(jìn)行讀寫時(shí),我們需要有種方法能夠保證對(duì)
    的頭像 發(fā)表于 01-10 11:16 ?6130次閱讀
    基于<b class='flag-5'>多線程</b>環(huán)境<b class='flag-5'>下</b><b class='flag-5'>值</b>的遞增<b class='flag-5'>操作</b>--原子<b class='flag-5'>操作</b>

    Linux多線程編程

    線程呢?使用多線程到底有哪些好處?什么的系統(tǒng)應(yīng)該選用多線程?我們首先必須回答這些問題?! ∈褂?b class='flag-5'>多線程的理由之是和進(jìn)程相比,它是
    發(fā)表于 04-02 14:43 ?593次閱讀

    怎樣才能在不加鎖的情況下解決多線程問題

    我們知道,多線程同時(shí)修改共享變量時(shí)會(huì)出現(xiàn)數(shù)據(jù)不致的問題,比如多個(gè)線程同時(shí)對(duì)個(gè)變量加1,假設(shè)count的初始
    的頭像 發(fā)表于 03-02 09:31 ?452次閱讀
    怎樣才能在不加鎖的<b class='flag-5'>情況下</b>解決<b class='flag-5'>多線程</b>問題

    基于QT自制上位機(jī)(多線程

    前言:應(yīng)用程序某些情況下需要處理比較復(fù)雜的邏輯,例如常規(guī)的圖傳上位機(jī),如果在傳輸圖片跑到較高碼流或?qū)D像執(zhí)行些處理任務(wù)是,引用多線程可以明顯 改善響應(yīng)度和反饋速度。 QT
    發(fā)表于 05-09 11:47 ?1次下載
    基于QT自制上位機(jī)(<b class='flag-5'>多線程</b>)

    多線程事務(wù)怎么回滾?個(gè)簡(jiǎn)單示例演示多線程事務(wù)

    spring中可以使用@Transactional注解去控制事務(wù),使出現(xiàn)異常時(shí)會(huì)進(jìn)行回滾,多線程中,這個(gè)注解則不會(huì)生效,如果主線程需要先
    發(fā)表于 08-09 12:22 ?646次閱讀
    <b class='flag-5'>多線程</b>事務(wù)怎么回滾?<b class='flag-5'>一</b><b class='flag-5'>個(gè)</b>簡(jiǎn)單示例演示<b class='flag-5'>多線程</b>事務(wù)

    什么情況下避免使用系統(tǒng)調(diào)用

    linux多線程環(huán)境對(duì)同變量進(jìn)行讀寫時(shí),經(jīng)常會(huì)遇到讀寫的原子性問題,即會(huì)出現(xiàn)競(jìng)爭(zhēng)條件。為了解決多個(gè)
    的頭像 發(fā)表于 11-13 10:32 ?409次閱讀
    什么<b class='flag-5'>情況下</b>避免使用系統(tǒng)調(diào)用

    c語言中a++是什么意思

    C語言中,a++個(gè)自增運(yùn)算符,用于對(duì)a進(jìn)行
    的頭像 發(fā)表于 11-26 09:19 ?1.7w次閱讀