讓你徹底理解Synchronized

原創(chuàng)文章&經(jīng)驗總結(jié)&從校招到A廠一路陽光一路滄桑

詳情請戳www.codercc.com

image

1. synchronized簡介

在學(xué)習(xí)知識前,我們先來看一個現(xiàn)象:

public class SynchronizedDemo implements Runnable {
    private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new SynchronizedDemo());
            thread.start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + count);
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000000; i++)
            count++;
    }
}

開啟了10個線程,每個線程都累加了1000000次,如果結(jié)果正確的話自然而然總數(shù)就應(yīng)該是10 * 1000000 = 10000000??删瓦\行多次結(jié)果都不是這個數(shù),而且每次運行結(jié)果都不一樣。這是為什么了?有什么解決方案了?這就是我們今天要聊的事情。

在上一篇博文中我們已經(jīng)了解了java內(nèi)存模型的一些知識,并且已經(jīng)知道出現(xiàn)線程安全的主要來源于JMM的設(shè)計,主要集中在主內(nèi)存和線程的工作內(nèi)存而導(dǎo)致的內(nèi)存可見性問題,以及重排序?qū)е碌膯栴},進(jìn)一步知道了happens-before規(guī)則。線程運行時擁有自己的??臻g,會在自己的??臻g運行,如果多線程間沒有共享的數(shù)據(jù)也就是說多線程間并沒有協(xié)作完成一件事情,那么,多線程就不能發(fā)揮優(yōu)勢,不能帶來巨大的價值。那么共享數(shù)據(jù)的線程安全問題怎樣處理?很自然而然的想法就是每一個線程依次去讀寫這個共享變量,這樣就不會有任何數(shù)據(jù)安全的問題,因為每個線程所操作的都是當(dāng)前最新的版本數(shù)據(jù)。那么,在java關(guān)鍵字synchronized就具有使每個線程依次排隊操作共享變量的功能。很顯然,這種同步機制效率很低,但synchronized是其他并發(fā)容器實現(xiàn)的基礎(chǔ),對它的理解也會大大提升對并發(fā)編程的感覺,從功利的角度來說,這也是面試高頻的考點。好了,下面,就來具體說說這個關(guān)鍵字。

2. synchronized實現(xiàn)原理

在java代碼中使用synchronized可是使用在代碼塊和方法中,根據(jù)synchronized用的位置可以有如表3.1這些使用場景:

使用位置 作用范圍 被鎖的對象 示例代碼
方法 實例方法 類的實例對象 public synchronized void method() { .......}
靜態(tài)方法 類對象 public static synchronized void method1() { .......}
代碼塊 實例對象 類的實例對象 synchronized (this) { .......}
class對象 類對象 synchronized (SynchronizedScopeDemo.class) { .......}
任意實例對象object 實例對象object final String lock = "";synchronized (lock) { .......}

synchronized可以用在方法上也可以使用在代碼塊中,方法是實例方法和靜態(tài)方法分別鎖的是該類的實例對象和該類的對象。而使用在代碼塊中根據(jù)鎖的目標(biāo)對象

也可以分為三種,具體的可以看表數(shù)據(jù)。這里的需要注意的是如果鎖的是類對象的話,盡管new多個實例對象,依然會被鎖住。synchronized的使用起來很簡單,那么背后的原理以及實現(xiàn)機制是怎樣的呢?

1 對象鎖(monitor)機制

現(xiàn)在來進(jìn)一步分析synchronized的具體底層實現(xiàn),有如下一個簡單的示例代碼:

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
            System.out.println("hello synchronized!");
        }
    }
}

上述代碼通過synchronized“鎖住”當(dāng)前類對象來進(jìn)行同步,將java代碼進(jìn)行編譯之后通過javap -v SynchronizedDemo .class來查看對應(yīng)的main方法字節(jié)碼如下:

public static void main(java.lang.String[]);

    descriptor: ([Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

     stack=2, locals=3, args_size=1

         0: ldc           #2                  // class com/codercc/chapter3/SynchronizedDemo

         2: dup

         3: astore_1

         4: **monitorenter**

         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;

         8: ldc           #4                  // String hello synchronized!

        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

        13: aload_1

        14: **monitorexit**

        15: **goto**          23

        18: astore_2

        19: aload_1

        20: **monitorexit**

        21: aload_2

        22: **athrow**

        23: **return

重要的字節(jié)碼已經(jīng)在原字節(jié)碼文件中進(jìn)行了標(biāo)注,再進(jìn)入到synchronized同步塊中,需要通過monitorenter指令獲取到對象的monitor(也通常稱之為對象鎖)后才能往下進(jìn)行執(zhí)行,在處理完對應(yīng)的方法內(nèi)部邏輯之后通過monitorexit指令來釋放所持有的monitor,以供其他并發(fā)實體進(jìn)行獲取。代碼后續(xù)執(zhí)行到第15行g(shù)oto語句進(jìn)而繼續(xù)到第23行return指令,方法成功執(zhí)行退出。另外當(dāng)方法異常的情況下,如果monitor不進(jìn)行釋放,對其他阻塞對待的并發(fā)實體來說就一直沒有機會獲取到了,系統(tǒng)會形成死鎖狀態(tài)很顯然這樣是不合理。

因此針對異常的情況,會執(zhí)行到第20行指令通過monitorexit釋放monitor鎖,進(jìn)一步通過第22行字節(jié)碼athrow拋出對應(yīng)的異常。從字節(jié)碼指令分析也可以看出在使用synchronized是具備隱式加鎖和釋放鎖的操作便利性的,并且針對異常情況也做了釋放鎖的處理。

每個對象都存在一個與之關(guān)聯(lián)的monitor,線程對monitor持有的方式以及持有時機決定了synchronized的鎖狀態(tài)以及synchronized的狀態(tài)升級方式。monitor是通過C++中ObjectMonitor實現(xiàn),代碼可以通過openjdk hotspot鏈接(http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/ )進(jìn)行下載openjdk中hotspot版本的源碼,具體文件路徑在src\share\vm\runtime\objectMonitor.hpp,具體源碼為:

  // initialize the monitor, exception the semaphore, all other fields

  // are simple integers or pointers

  ObjectMonitor() {

    _header       = NULL;

    _count        = 0;

    _waiters      = 0,

    _recursions   = 0;

    _object       = NULL;

    _owner        = NULL;

    **_WaitSet**      = NULL;

    _WaitSetLock  = 0 ;

    _Responsible  = NULL ;

    _succ         = NULL ;

    _cxq          = NULL ;

    FreeNext      = NULL ;

    **_EntryList**    = NULL ;

    _SpinFreq     = 0 ;

    _SpinClock    = 0 ;

    OwnerIsThread = 0 ;

    _previous_owner_tid = 0;

  }

從ObjectMonitor的結(jié)構(gòu)中可以看出主要維護_WaitSet以及_EntryList兩個隊列來保存ObjectWaiter 對象,當(dāng)每個阻塞等待獲取鎖的線程都會被封裝成ObjectWaiter對象來進(jìn)行入隊,與此同時如果獲取到鎖資源的話就會出隊操作。另外_owner則指向當(dāng)前持有ObjectMonitor對象的線程。等待獲取鎖以及獲取鎖出隊的示意圖如下圖所示:

image.png

當(dāng)多個線程進(jìn)行獲取鎖的時候,首先都會進(jìn)行_EntryList隊列,其中一個線程獲取到對象的monitor后,對monitor而言就會將_owner變量設(shè)置為當(dāng)前線程,并且monitor維護的計數(shù)器就會加1。如果當(dāng)前線程執(zhí)行完邏輯并退出后,monitor中_owner變量就會清空并且計數(shù)器減1,這樣就能讓其他線程能夠競爭到monitor。另外,如果調(diào)用了wait()方法后,當(dāng)前線程就會進(jìn)入到_WaitSet中等待被喚醒,如果被喚醒并且執(zhí)行退出后,也會對狀態(tài)量進(jìn)行重置,也便于其他線程能夠獲取到monitor。

從線程狀態(tài)變化的角度來看,如果要想進(jìn)入到同步塊或者執(zhí)行同步方法,都需要先獲取到對象的monitor,如果獲取不到則會變更為BLOCKED狀態(tài),具體過程如下圖所示:

image.png

從上圖可以看出任意線程對Object的訪問,首先要獲得Object的monitor,如果獲取失敗,該線程就會進(jìn)入到同步隊列中,線程狀態(tài)變?yōu)锽LOCKED。當(dāng)monitor持有者釋放后,在同步隊列中的線程才會有機會重新獲取monitor,才能繼續(xù)執(zhí)行。

2 synchronized的happens-before關(guān)系

在第2章中分析過happens-before規(guī)則,其中有一條就是監(jiān)視器鎖規(guī)則:對同一個監(jiān)視器的解鎖happens-before于對該監(jiān)視器的加鎖。為了進(jìn)一步了解synchronized的并發(fā)語義,通過示例代碼分析這條happens-before規(guī)則,示例代碼如下:

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}

在并發(fā)時,第5步操作中讀取到的變量a的值是多少呢?這就需要通過happens-before規(guī)則來進(jìn)行分析,示例代碼的happens-before關(guān)系如下圖所示:

image.png

上圖中每一個箭頭連接的兩個節(jié)點就代表之間的happens-before關(guān)系,黑色的是通過程序順序規(guī)則推導(dǎo)出來,通過監(jiān)視器鎖規(guī)則可以推導(dǎo)出線程A釋放鎖happens-before線程B加鎖,即紅色線表示。藍(lán)色的線則是通過傳遞性規(guī)則進(jìn)一步推導(dǎo)的happens-before關(guān)系。最終得到的結(jié)論就是操作2 happens-before 5,通過這個關(guān)系可以得出什么?

根據(jù)happens-before的定義中的一條:如果A happens-before B,則A的執(zhí)行結(jié)果對B可見。那么在該示例代碼中,線程A先對共享變量A進(jìn)行加1,由2 happens-before 5關(guān)系可知線程A的執(zhí)行結(jié)果對線程B可見即線程B所讀取到的a的值為1。

3 鎖獲取和鎖釋放的內(nèi)存語義

在第2章中總結(jié)果JMM核心為兩個部分:happens-before規(guī)則以及內(nèi)存抽象模型。在分析完synchronized的happens-before關(guān)系后還是不太完整的,接下來看看基于java內(nèi)存抽象模型的synchronized的內(nèi)存語義,具體過程如下圖所示:

image.png

針對線程A的操作而言,從上圖可以看出線程A會首先先從主內(nèi)存中讀取共享變量a=0的值然后將該變量拷貝到線程本地內(nèi)存。然后基于該值進(jìn)行數(shù)據(jù)操作后變量a變?yōu)?,然后會將值寫入到主內(nèi)存中。

image.png

對線程B而言執(zhí)行流程如上圖所示。線程B獲取鎖的時候會強制從主內(nèi)存中共享變量a的值,而此時變量a已經(jīng)是最新值了。接下來線程B會將該值拷貝到工作內(nèi)存中進(jìn)行操作,同樣的執(zhí)行完操作后也會重新寫入到主內(nèi)存中。

從橫向來看,線程A和線程線程都是基于主內(nèi)存中的共享變量互相感知到對方的數(shù)據(jù)操作,并基于共享變量來完成并發(fā)實體中的協(xié)同工作,整個過程就好像線程A給線程B發(fā)送了一個數(shù)據(jù)變更的“通知”,這種通信機制就是基于共享內(nèi)存的并發(fā)模型結(jié)構(gòu)導(dǎo)致。

通過上面的討論對synchronized應(yīng)該有一定了解,它最大的特征就是在同一時刻只有一個線程能夠獲得對象monitor,從而確保當(dāng)前線程能夠執(zhí)行到相應(yīng)的同步邏輯,對線程之間而言表現(xiàn)為互斥性(排它性)。自然而然這種同步方式會有效率相對低下的弊端,既然同步流程不能發(fā)生改變,那么能不能讓每次獲取鎖的速度更快或者降低阻塞等待的概率呢?也就是通過局部的優(yōu)化來提升系統(tǒng)整體的并發(fā)同步的效率。比如去收銀臺付款的場景,之前的方式是大家都去排隊,然后去紙幣付款收銀員找零。甚至有的時候付款的時候還需要在包里拿出錢包拿出錢,這個過程是比較耗時的。針對付款的流程,就可以通過線上化的手段來進(jìn)行優(yōu)化,在現(xiàn)在只需要通過支付寶掃描二維碼就可以完成付款了,也省去了收銀員找零的時間。盡管整個付款場景還是需要排隊,但是因為付款(類似于獲取鎖釋放鎖)這個環(huán)節(jié)的優(yōu)化導(dǎo)致耗時大大縮短,對收銀臺(系統(tǒng)整體并發(fā)效率)而言操作效率就極大的帶來提升。如此類比,如果能對鎖操作過程進(jìn)行優(yōu)化的話,也會對并發(fā)效率帶來極大的提升。

那么,針對synchronized的優(yōu)化是怎樣做的呢?在進(jìn)一步分析之前,需要先了解這兩個概念:1. CAS操作;2.Java對象頭。

3.1 CAS操作

3.1.1 什么是CAS?

使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設(shè)每一次執(zhí)行臨界區(qū)代碼都會產(chǎn)生沖突,所以當(dāng)前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略,它假設(shè)所有線程訪問共享資源的時候不會出現(xiàn)沖突,既然不會出現(xiàn)沖突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現(xiàn)阻塞停頓的狀態(tài)。那么,如果出現(xiàn)沖突了怎么辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現(xiàn)沖突,出現(xiàn)沖突就重試當(dāng)前操作直到?jīng)]有沖突為止。

3.1.2 CAS的操作過程

CAS比較交換的過程可以通俗的理解為CAS(V,O,N),包含三個值分別為:V 內(nèi)存地址存放的實際值;O 預(yù)期的值(舊值);N 更新的新值。當(dāng)V和O相同時,也就是說舊值和內(nèi)存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經(jīng)被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當(dāng)多個線程使用CAS操作一個變量是,只有一個線程會成功,并成功更新,其余會失敗。失敗的線程會重新嘗試,當(dāng)然也可以選擇掛起線程

CAS的實現(xiàn)需要硬件指令集的支撐,在JDK1.5后虛擬機才可以使用處理器提供的CMPXCHG指令實現(xiàn)。

Synchronized VS CAS

元老級的Synchronized(未優(yōu)化前)最主要的問題是:在存在線程競爭的情況下會出現(xiàn)線程阻塞和喚醒鎖帶來的性能問題,因為這是一種互斥同步(阻塞同步)。而CAS并不是武斷的間線程掛起,當(dāng)CAS操作失敗后會進(jìn)行一定的嘗試,而非進(jìn)行耗時的掛起喚醒的操作,因此也叫做非阻塞同步。這是兩者主要的區(qū)別。

3.1.3 CAS的應(yīng)用場景

在J.U.C包中利用CAS實現(xiàn)類有很多,可以說是支撐起整個concurrency包的實現(xiàn),在Lock實現(xiàn)中會有CAS改變state變量,在atomic包中的實現(xiàn)類也幾乎都是用CAS實現(xiàn),關(guān)于這些具體的實現(xiàn)場景在之后會詳細(xì)聊聊,現(xiàn)在有個印象就好了(微笑臉)。

3.1.4 CAS的問題

1. ABA問題
因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題。比如一個舊值A(chǔ)變?yōu)榱顺葿,然后再變成A,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A,但是實際上的確發(fā)生了變化。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式,添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C。java這么優(yōu)秀的語言,當(dāng)然在java 1.5后的atomic包中提供了AtomicStampedReference來解決ABA問題,解決思路就是這樣的。

2. 自旋時間過長

使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(無非就是一個死循環(huán))進(jìn)行下一次嘗試,如果這里自旋時間過長對性能是很大的消耗。如果JVM能支持處理器提供的pause指令,那么在效率上會有一定的提升。

3. 只能保證一個共享變量的原子操作

當(dāng)對一個共享變量執(zhí)行操作時CAS能保證其原子性,如果對多個共享變量進(jìn)行操作,CAS就不能保證其原子性。有一個解決方案是利用對象整合多個共享變量,即一個類中的成員變量就是這幾個共享變量。然后將這個對象做CAS操作就可以保證其原子性。atomic中提供了AtomicReference來保證引用對象之間的原子性。

3.2 Java對象頭

在同步的時候是獲取對象的monitor,即獲取到對象的鎖。那么對象的鎖怎么理解?無非就是類似對對象的一個標(biāo)志,那么這個標(biāo)志就是存放在Java對象的對象頭。Java對象頭里的Mark Word里默認(rèn)的存放的對象的Hashcode,分代年齡和鎖標(biāo)記位。32為JVM Mark Word默認(rèn)存儲結(jié)構(gòu)為(注:java對象頭以及下面的鎖狀態(tài)變化摘自《java并發(fā)編程的藝術(shù)》一書,該書我認(rèn)為寫的足夠好,就沒在自己組織語言班門弄斧了):

Mark Word存儲結(jié)構(gòu)

如圖在Mark Word會默認(rèn)存放hasdcode,年齡值以及鎖標(biāo)志位等信息。

Java SE 1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾個狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。對象的MarkWord變化為下圖:

Mark Word狀態(tài)變化

3.2 偏向鎖

HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。

偏向鎖的獲取

當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程

偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當(dāng)其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。

偏向鎖撤銷流程

如圖,偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼)。它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態(tài),則將對象頭設(shè)置成無鎖狀態(tài);如果線程仍然活著,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖,最后喚醒暫停的線程。

下圖線程1展示了偏向鎖獲取的過程,線程2展示了偏向鎖撤銷的過程。

偏向鎖獲取和撤銷流程

如何關(guān)閉偏向鎖

偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)

3.3 輕量級鎖

加鎖

線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖

解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,導(dǎo)致鎖膨脹的流程圖。

輕量級鎖加鎖解鎖以及鎖膨脹

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進(jìn)行新一輪的奪鎖之爭。

3.5 各種鎖的比較

各種鎖的對比

4. 一個例子

經(jīng)過上面的理解,我們現(xiàn)在應(yīng)該知道了該怎樣解決了。更正后的代碼為:

public class SynchronizedDemo implements Runnable {
    private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new SynchronizedDemo());
            thread.start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + count);
    }

    @Override
    public void run() {
        synchronized (SynchronizedDemo.class) {
            for (int i = 0; i < 1000000; i++)
                count++;
        }
    }
}

開啟十個線程,每個線程在原值上累加1000000次,最終正確的結(jié)果為10X1000000=10000000,這里能夠計算出正確的結(jié)果是因為在做累加操作時使用了同步代碼塊,這樣就能保證每個線程所獲得共享變量的值都是當(dāng)前最新的值,如果不使用同步的話,就可能會出現(xiàn)A線程累加后,而B線程做累加操作有可能是使用原來的就值,即“臟值”。這樣,就導(dǎo)致最終的計算結(jié)果不是正確的。而使用Syncnized就可能保證內(nèi)存可見性,保證每個線程都是操作的最新值。這里只是一個示例性的demo,聰明的你,還有其他辦法嗎?

參考文獻(xiàn)

《java并發(fā)編程的藝術(shù)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,917評論 0 11
  • 1. synchronized簡介 在學(xué)習(xí)知識前,我們先來看一個現(xiàn)象: 開啟了10個線程,每個線程都累加了1000...
    朦朧蜜桃閱讀 578評論 0 0
  • 很多時候我們都以為自己已經(jīng)聽懂對方的意思的時候,回來自己操作才發(fā)現(xiàn)并不是自己認(rèn)為的那樣。(有沒有淚奔) ...
    不不忘初心閱讀 960評論 0 1
  • 今天我們用問題來審視自己與他人的關(guān)系。我預(yù)設(shè)了分?jǐn)?shù),結(jié)果和預(yù)設(shè)很相近。但是當(dāng)我沉下心來思考每一個問題的時候,我突然...
    風(fēng)小懶閱讀 368評論 2 2
  • 前段日子,我的一位表侄女向我訴說:“我工作一年了,沒有一分存款”。 侄女,在一家裝潢公司上班,做室內(nèi)設(shè)計,從去年暑...
    木里兮閱讀 978評論 4 3

友情鏈接更多精彩內(nèi)容