計數器

講講Java里計數器問題,對于理解原子性很有幫助。

線程安全的計數器 與 線程不安全的計數器

直接上代碼,代碼里實現了兩種計數器,SafeCounterNonSafeCounter,顧名思義,前者是線程安全的,后者是線程不安全的。
線程安全的計數器內部使用了AtomicLong,它的自增操作是原子性的。
而線程不安全的計數器直接使用了Long,它連單次讀,或單次寫,都不是原子性的(加上volatile關鍵字可使得單次讀,或單次寫具有原子性,同樣情況的還有Double),那就更別提自增了(自增相當于一次讀加一次寫)

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();    
    }    
} 

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

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());    
    }    
}    

最后的打印結果:

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)來保證計數器的正確性。

加個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不能保證自增操作的原子性。
但比較有趣的時,多跑幾次代碼你會發現,加了volatile關鍵字,最后count出來的值總是大于沒加volatile關鍵字(雖然都不正確)的時候。我覺得一個合理的解釋是,volatile保證讀寫都在主存上(可見性),而沒加volatile時,多個線程在做自增操作時是在cpu的寄存器里,這樣自然漏加很多。
到這里,我覺得引出了兩個問題:

  • 線程安全的計數器,還有其它的實現嗎?不同實現有什么區別?
  • AtomicLong 如何保證自增操作的原子性?

線程安全計數器 不同實現

先來說說上面提到的第一個問題
除了用AtomicLong來實現線程安全的計數器,大家肯定也很容易想到用synchronizedLock
上代碼,SafeCounter_1 SafeCounter_2 SafeCounter_3,分別使用了synchronizedLockAtomicLong 來實現線程安全的計數器

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();    
    }    
}

為了測試三種不同實現的性能好壞,加上程序運行的時間

    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)是多線程去計數的邏輯,為了能直觀的體現出性能的好壞,把單個線程count的數量加到了100000(final int loopcount = 100000),線程數加到了100(int threadcount = 100)
Thread.sleep(300);是為了讓Main Thread在其它線程都完全返回后再執行。

    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());
    }

好了,在我的環境上,使用SafeCounter_1,多次運行,發現執行時間基本在870ms - 920ms這個區間

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

使用SafeCounter_2,運行時間基本在620ms - 650ms這個區間

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

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

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

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

AtomicLong

前面我們看到,用AtomicLong來實現計數器時,調用了方法atomicLong.incrementAndGet(),這個方法做的就是一個自增操作,而且這個方法是原子性的,它如何做到的呢?網上看到incrementAndGet()的源碼,雖然應該是AtomicInteger的代碼,但思想應該一樣:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
  • AtomicLong的各種操作,通過CAS來保證原子性:
    compareAndSet(current, next)即是CAS了,簡單的說,它通過比較前值是否和內存中一樣,來決定是否更新。
    前面說到了自增包括一次讀,一次寫,這里先“取原值”(int current = get()),然后“計算”(int next = current + 1),照理說接下來就該寫了。但多線程環境下誰也無法保證在"取原值"和"計算"期間是否有其它線程已對“原值”做出了修改,那怎么辦?
    CAS通過比較之前取出的“原值”和內存中的實際值,來確定是否有來自其它線程的更新,如果相同就說明沒有其它線程的更新,那接著就寫入。如果不相同,那簡單,你重新跑次。
  • AtomicLong 通過樂觀鎖的方式,使得性能更好
    其實上面這種CAS加循環的方式就實現了一個“樂觀鎖”,相比“悲觀鎖”的實現(Lock synchronized),“樂觀鎖”認為線程沖突總是少的,如果有沖突我就回退重跑,那這樣就節省了“悲觀鎖”里線程間競爭鎖的開銷。

我們都知道,cpu是時分復用的,也就是把cpu的時間片,分配給不同的thread/process輪流執行,時間片與時間片之間,需要進行cpu切換,也就是會發生進程的切換。切換涉及到清空寄存器,緩存數據。然后重新加載新的thread所需數據。當一個線程被掛起時,加入到阻塞隊列,在一定的時間或條件下,在通過notify(),notifyAll()喚醒回來。在某個資源不可用的時候,就將cpu讓出,把當前等待線程切換為阻塞狀態。等到資源(比如一個共享數據)可用了,那么就將線程喚醒,讓他進入runnable狀態等待cpu調度。這就是典型的悲觀鎖的實現。獨占鎖是一種悲觀鎖,synchronized就是一種獨占鎖,它假設最壞的情況,并且只有在確保其它線程不會造成干擾的情況下執行,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。

但是,由于在進程掛起和恢復執行過程中存在著很大的開銷。當一個線程正在等待鎖時,它不能做任何事,所以悲觀鎖有很大的缺點。舉個例子,如果一個線程需要某個資源,但是這個資源的占用時間很短,當線程第一次搶占這個資源時,可能這個資源被占用,如果此時掛起這個線程,可能立刻就發現資源可用,然后又需要花費很長的時間重新搶占鎖,時間代價就會非常的高。

所以就有了樂觀鎖的概念,他的核心思路就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。在上面的例子中,某個線程可以不讓出cpu,而是一直while循環,如果失敗就重試,直到成功為止。所以,當數據爭用不嚴重時,樂觀鎖效果更好。比如CAS就是一種樂觀鎖思想的應用。

ABA問題

CAS看似不錯,但也有自己的問題,那就是ABA問題。
簡單的說就是,在1號線程“取原值”和“CAS操作”中間,2號線程把“原值”A改為B,然后又從B改為A,那1號線程在接著做“CAS操作”時,發現內存中還是A,就繼續做下去。然而此時已違反了原子性。
解決這個問題的方法其實也很簡單,帶個版本修改信息。
Java CAS 和ABA問題

關鍵字

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

Code:

Sample Code on Github

參考:

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區別 13、...
    Miley_MOJIE閱讀 3,731評論 0 11
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,366評論 11 349
  • layout: posttitle: 《Java并發編程的藝術》筆記categories: Javaexcerpt...
    xiaogmail閱讀 5,864評論 1 19
  • 一個計數器通常是由一組觸發器構成,該組觸發器按照預先給定的順序改變其狀態,如果所有觸發器的狀態改變是在同一時鐘脈沖...
    錦穗閱讀 13,681評論 0 6
  • “邵子牙貢丸.魚丸”是我在鼓浪嶼上遇到的最滿意的一家店了。 由于避開了端午的高峰,所以當我從龍頭路上溜溜達達走到這...
    青年西米閱讀 5,654評論 7 18