計數(shù)器

講講Java里計數(shù)器問題,對于理解原子性很有幫助蚓再。

線程安全的計數(shù)器 與 線程不安全的計數(shù)器

直接上代碼绎秒,代碼里實現(xiàn)了兩種計數(shù)器,SafeCounterNonSafeCounter丝里,顧名思義,前者是線程安全的体谒,后者是線程不安全的杯聚。
線程安全的計數(shù)器內(nèi)部使用了AtomicLong,它的自增操作是原子性的抒痒。
而線程不安全的計數(shù)器直接使用了Long幌绍,它連單次讀,或單次寫故响,都不是原子性的(加上volatile關(guān)鍵字可使得單次讀傀广,或單次寫具有原子性,同樣情況的還有Double)彩届,那就更別提自增了(自增相當(dāng)于一次讀加一次寫)

class NonSafeCounter{    
    private long count = 0;    
    public void increase()    
    {    
        count++;    
    }    
    
    public long  get()    
    {    
        return count;    
    }    
}    
    
class SafeCounter{    
    private AtomicLong atomicLong  = new AtomicLong(0);
    public void increase()    
    {    
        atomicLong.incrementAndGet();
    }    
    
    public long get()    
    {    
        return atomicLong.longValue();    
    }    
} 

主函數(shù)無非就是多線程去使用Counter(SafeCounterNonSafeCounter)去計數(shù)

public class CounterTest {    
    
    public static void main(String[] args) throws InterruptedException, BrokenBarrierException    
    {    
        final int loopcount = 10000;    
        int threadcount = 10;    
        //Non Safe
        final NonSafeCounter nonSafeCounter = new NonSafeCounter();    
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(threadcount + 1);
        for(int i = 0; i < threadcount; ++i)    
        {
            final int index = i; 
            new Thread(new Runnable() {    
                @Override    
                public void run() {    
                    for(int j = 0; j < loopcount; ++j)    
                    {    
                        nonSafeCounter.increase();
                    }
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread Finished: " + index);
                }    
            }).start();    
        }
        cyclicBarrier.await();
        Thread.sleep(300);
        System.out.println("NonSafeCounter:" + nonSafeCounter.get());    
    
        
        //Safe
        final SafeCounter safeCounter = new SafeCounter();    
        for(int i = 0; i < threadcount; ++i)    
        {
            final int index = i; 
            new Thread(new Runnable() {    
                @Override    
                public void run() {    
                    for(int j = 0; j < loopcount; ++j)    
                    {    
                        safeCounter.increase();
                    }
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread Finished: " + index);
                }    
            }).start();    
        }
        cyclicBarrier.await();
        Thread.sleep(300);
        System.out.println("SafeCounter:" + safeCounter.get());    
    }    
}    

最后的打印結(jié)果:

Thread Finished: 8
Thread Finished: 1
Thread Finished: 2
Thread Finished: 7
Thread Finished: 6
Thread Finished: 0
Thread Finished: 5
Thread Finished: 9
Thread Finished: 3
Thread Finished: 4
NonSafeCounter:39180
Thread Finished: 8
Thread Finished: 2
Thread Finished: 4
Thread Finished: 6
Thread Finished: 1
Thread Finished: 5
Thread Finished: 9
Thread Finished: 3
Thread Finished: 7
Thread Finished: 0
SafeCounter:100000

可以看出伪冰,多線程情況下,必須要使用一些同步策略(此處是AtomicLong)來保證計數(shù)器的正確性樟蠕。

加個volatile試試

上面說到了贮聂,volatile不能保證自增操作(如count++)的原子性,還是試驗下寨辩,給NonSafeCounter加上volatile吓懈,然后重新運行

class NonSafeCounter{    
    private volatile long count = 0;    
    public void increase()    
    {    
        count++;    
    }    
    
    public long  get()    
    {    
        return count;    
    }    
}  

輸出:

Thread Finished: 8
Thread Finished: 1
Thread Finished: 7
Thread Finished: 6
Thread Finished: 0
Thread Finished: 2
Thread Finished: 9
Thread Finished: 3
Thread Finished: 4
Thread Finished: 5
NonSafeCounter:49017
Thread Finished: 8
Thread Finished: 2
Thread Finished: 1
Thread Finished: 0
Thread Finished: 3
Thread Finished: 6
Thread Finished: 9
Thread Finished: 4
Thread Finished: 7
Thread Finished: 5
SafeCounter:100000

這個輸出說明了,我沒有騙大家靡狞,volatile不能保證自增操作的原子性耻警。
但比較有趣的時,多跑幾次代碼你會發(fā)現(xiàn),加了volatile關(guān)鍵字甘穿,最后count出來的值總是大于沒加volatile關(guān)鍵字(雖然都不正確)的時候腮恩。我覺得一個合理的解釋是,volatile保證讀寫都在主存上(可見性)扒磁,而沒加volatile時庆揪,多個線程在做自增操作時是在cpu的寄存器里,這樣自然漏加很多妨托。
到這里缸榛,我覺得引出了兩個問題:

  • 線程安全的計數(shù)器,還有其它的實現(xiàn)嗎兰伤?不同實現(xiàn)有什么區(qū)別内颗?
  • AtomicLong 如何保證自增操作的原子性?

線程安全計數(shù)器 不同實現(xiàn)

先來說說上面提到的第一個問題
除了用AtomicLong來實現(xiàn)線程安全的計數(shù)器敦腔,大家肯定也很容易想到用synchronizedLock
上代碼均澳,SafeCounter_1 SafeCounter_2 SafeCounter_3,分別使用了synchronized符衔,LockAtomicLong 來實現(xiàn)線程安全的計數(shù)器

interface SafeCounterI{
    public void increase();  
    public long get();
}
class SafeCounter_1 implements SafeCounterI{    
    private long count = 0;    
    public synchronized void increase()    
    {    
        count++;    
    }    
    public long  get()    
    {    
        return count;    
    }    
}
class SafeCounter_2 implements SafeCounterI{    
    private long count = 0;
    Lock lock = new ReentrantLock();
    public void increase()    
    {   
        try{
            lock.lock();
            count++;    
        }finally{
            lock.unlock();
        }
    }    
    public long  get()    
    {    
        return count;    
    }    
}
class SafeCounter_3 implements SafeCounterI{    
    private AtomicLong atomicLong  = new AtomicLong(0);
    public void increase()    
    {    
        atomicLong.incrementAndGet();
    }    
    public long get()    
    {    
        return atomicLong.longValue();    
    }    
}

為了測試三種不同實現(xiàn)的性能好壞找前,加上程序運行的時間

    public static void main(String[] args) throws Exception    
    {   
        Long start = System.currentTimeMillis();
        final SafeCounterI safeCounter= new SafeCounter_1();  
        multiThreadCount(safeCounter);
        System.out.println(System.currentTimeMillis() - start);
        
    }

multiThreadCount(safeCounter)是多線程去計數(shù)的邏輯,為了能直觀的體現(xiàn)出性能的好壞判族,把單個線程count的數(shù)量加到了100000(final int loopcount = 100000)躺盛,線程數(shù)加到了100(int threadcount = 100)
Thread.sleep(300);是為了讓Main Thread在其它線程都完全返回后再執(zhí)行。

    private static void multiThreadCount(final SafeCounterI safeCounter)
            throws InterruptedException, BrokenBarrierException {
        final int loopcount = 100000;    
        int threadcount = 100;    
        //Non Safe
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(threadcount + 1);
        for(int i = 0; i < threadcount; ++i)    
        {
            final int index = i; 
            new Thread(new Runnable() {    
                @Override    
                public void run() {    
                    for(int j = 0; j < loopcount; ++j)    
                    {    
                        safeCounter.increase();
                    }
                    try {
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread Finished: " + index);
                }    
            }).start();    
        }
        cyclicBarrier.await();
        Thread.sleep(300);
        System.out.println("NonSafeCounter:" + safeCounter.get());
    }

好了形帮,在我的環(huán)境上槽惫,使用SafeCounter_1,多次運行辩撑,發(fā)現(xiàn)執(zhí)行時間基本在870ms - 920ms這個區(qū)間

...
Thread Finished: 95
Thread Finished: 81
NonSafeCounter:10000000
884

使用SafeCounter_2界斜,運行時間基本在620ms - 650ms這個區(qū)間

Thread Finished: 66
Thread Finished: 35
NonSafeCounter:10000000
638

而使用SafeCounter_3,運行時間基本在460ms - 500ms這個區(qū)間

Thread Finished: 39
Thread Finished: 42
NonSafeCounter:10000000
478

那結(jié)論就出來了合冀,性能上AtomicLong 好于 Lock 好于 synchronized
那為什么AtomicLong性能好各薇?
同樣,還有之前的問題:AtomicLong 如何保證自增操作的原子性君躺?

AtomicLong

前面我們看到峭判,用AtomicLong來實現(xiàn)計數(shù)器時,調(diào)用了方法atomicLong.incrementAndGet()晰洒,這個方法做的就是一個自增操作朝抖,而且這個方法是原子性的啥箭,它如何做到的呢谍珊?網(wǎng)上看到incrementAndGet()的源碼,雖然應(yīng)該是AtomicInteger的代碼,但思想應(yīng)該一樣:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
  • AtomicLong的各種操作砌滞,通過CAS來保證原子性:
    compareAndSet(current, next)即是CAS了侮邀,簡單的說,它通過比較前值是否和內(nèi)存中一樣贝润,來決定是否更新绊茧。
    前面說到了自增包括一次讀,一次寫打掘,這里先“取原值”(int current = get())华畏,然后“計算”(int next = current + 1),照理說接下來就該寫了尊蚁。但多線程環(huán)境下誰也無法保證在"取原值"和"計算"期間是否有其它線程已對“原值”做出了修改亡笑,那怎么辦?
    CAS通過比較之前取出的“原值”和內(nèi)存中的實際值,來確定是否有來自其它線程的更新横朋,如果相同就說明沒有其它線程的更新仑乌,那接著就寫入。如果不相同琴锭,那簡單晰甚,你重新跑次。
  • AtomicLong 通過樂觀鎖的方式决帖,使得性能更好
    其實上面這種CAS加循環(huán)的方式就實現(xiàn)了一個“樂觀鎖”厕九,相比“悲觀鎖”的實現(xiàn)(Lock synchronized),“樂觀鎖”認為線程沖突總是少的古瓤,如果有沖突我就回退重跑止剖,那這樣就節(jié)省了“悲觀鎖”里線程間競爭鎖的開銷。

我們都知道落君,cpu是時分復(fù)用的穿香,也就是把cpu的時間片,分配給不同的thread/process輪流執(zhí)行绎速,時間片與時間片之間皮获,需要進行cpu切換,也就是會發(fā)生進程的切換纹冤。切換涉及到清空寄存器洒宝,緩存數(shù)據(jù)。然后重新加載新的thread所需數(shù)據(jù)萌京。當(dāng)一個線程被掛起時雁歌,加入到阻塞隊列,在一定的時間或條件下知残,在通過notify()靠瞎,notifyAll()喚醒回來。在某個資源不可用的時候,就將cpu讓出乏盐,把當(dāng)前等待線程切換為阻塞狀態(tài)佳窑。等到資源(比如一個共享數(shù)據(jù))可用了,那么就將線程喚醒父能,讓他進入runnable狀態(tài)等待cpu調(diào)度神凑。這就是典型的悲觀鎖的實現(xiàn)。獨占鎖是一種悲觀鎖何吝,synchronized就是一種獨占鎖溉委,它假設(shè)最壞的情況,并且只有在確保其它線程不會造成干擾的情況下執(zhí)行爱榕,會導(dǎo)致其它所有需要鎖的線程掛起薛躬,等待持有鎖的線程釋放鎖。

但是呆细,由于在進程掛起和恢復(fù)執(zhí)行過程中存在著很大的開銷型宝。當(dāng)一個線程正在等待鎖時,它不能做任何事絮爷,所以悲觀鎖有很大的缺點趴酣。舉個例子,如果一個線程需要某個資源坑夯,但是這個資源的占用時間很短岖寞,當(dāng)線程第一次搶占這個資源時,可能這個資源被占用柜蜈,如果此時掛起這個線程仗谆,可能立刻就發(fā)現(xiàn)資源可用,然后又需要花費很長的時間重新?lián)屨兼i淑履,時間代價就會非常的高隶垮。

所以就有了樂觀鎖的概念,他的核心思路就是秘噪,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作狸吞,如果因為沖突失敗就重試,直到成功為止指煎。在上面的例子中蹋偏,某個線程可以不讓出cpu,而是一直while循環(huán),如果失敗就重試至壤,直到成功為止威始。所以,當(dāng)數(shù)據(jù)爭用不嚴重時像街,樂觀鎖效果更好黎棠。比如CAS就是一種樂觀鎖思想的應(yīng)用京郑。

ABA問題

CAS看似不錯,但也有自己的問題葫掉,那就是ABA問題。
簡單的說就是跟狱,在1號線程“取原值”和“CAS操作”中間俭厚,2號線程把“原值”A改為B,然后又從B改為A驶臊,那1號線程在接著做“CAS操作”時挪挤,發(fā)現(xiàn)內(nèi)存中還是A,就繼續(xù)做下去关翎。然而此時已違反了原子性扛门。
解決這個問題的方法其實也很簡單,帶個版本修改信息纵寝。
Java CAS 和ABA問題

關(guān)鍵字

  • AtomicLong
  • Lock
  • synchronized
  • volatile
  • CAS
  • ABA (加version解決)
  • 悲觀鎖 樂觀鎖

Code:

Sample Code on Github

參考:

線程安全并且無阻塞的Atomic類
淺析AtomicLong以及Unsafe
聊聊并發(fā)(五)——原子操作的實現(xiàn)原理
AtomicInteger源碼分析——基于CAS的樂觀鎖實現(xiàn)
JAVA-CAS簡介以及ABA問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末论寨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子爽茴,更是在濱河造成了極大的恐慌葬凳,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件室奏,死亡現(xiàn)場離奇詭異火焰,居然都是意外死亡,警方通過查閱死者的電腦和手機胧沫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門昌简,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绒怨,你說我怎么就攤上這事纯赎。” “怎么了南蹂?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵址否,是天一觀的道長。 經(jīng)常有香客問我碎紊,道長佑附,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任仗考,我火速辦了婚禮音同,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘秃嗜。我一直安慰自己权均,他們只是感情好顿膨,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叽赊,像睡著了一般恋沃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上必指,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天囊咏,我揣著相機與錄音,去河邊找鬼塔橡。 笑死梅割,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的葛家。 我是一名探鬼主播户辞,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼癞谒!你這毒婦竟也來了底燎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤弹砚,失蹤者是張志新(化名)和其女友劉穎书蚪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體迅栅,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡殊校,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了读存。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片为流。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖让簿,靈堂內(nèi)的尸體忽然破棺而出敬察,到底是詐尸還是另有隱情,我是刑警寧澤尔当,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布莲祸,位于F島的核電站,受9級特大地震影響椭迎,放射性物質(zhì)發(fā)生泄漏锐帜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一畜号、第九天 我趴在偏房一處隱蔽的房頂上張望缴阎。 院中可真熱鬧,春花似錦简软、人聲如沸蛮拔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽建炫。三九已至畦韭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肛跌,已是汗流浹背艺配。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惋砂,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓绳锅,卻偏偏與公主長得像西饵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鳞芙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容

  • Java8張圖 11眷柔、字符串不變性 12、equals()方法原朝、hashCode()方法的區(qū)別 13驯嘱、...
    Miley_MOJIE閱讀 3,701評論 0 11
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司喳坠,掛了不少鞠评,但最終還是拿到小米、百度壕鹉、阿里剃幌、京東、新浪晾浴、CVTE负乡、樂視家的研發(fā)崗...
    時芥藍閱讀 42,239評論 11 349
  • layout: posttitle: 《Java并發(fā)編程的藝術(shù)》筆記categories: Javaexcerpt...
    xiaogmail閱讀 5,815評論 1 19
  • 一個計數(shù)器通常是由一組觸發(fā)器構(gòu)成,該組觸發(fā)器按照預(yù)先給定的順序改變其狀態(tài)脊凰,如果所有觸發(fā)器的狀態(tài)改變是在同一時鐘脈沖...
    錦穗閱讀 13,289評論 0 6
  • “邵子牙貢丸.魚丸”是我在鼓浪嶼上遇到的最滿意的一家店了抖棘。 由于避開了端午的高峰,所以當(dāng)我從龍頭路上溜溜達達走到這...
    青年西米閱讀 5,564評論 7 18