講講Java里計數器問題,對于理解原子性很有幫助。
線程安全的計數器 與 線程不安全的計數器
直接上代碼,代碼里實現了兩種計數器,SafeCounter
和 NonSafeCounter
,顧名思義,前者是線程安全的,后者是線程不安全的。
線程安全的計數器內部使用了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(SafeCounter
和 NonSafeCounter
)去計數
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
來實現線程安全的計數器,大家肯定也很容易想到用synchronized
和Lock
上代碼,SafeCounter_1
SafeCounter_2
SafeCounter_3
,分別使用了synchronized
,Lock
和AtomicLong
來實現線程安全的計數器
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:
參考:
線程安全并且無阻塞的Atomic類
淺析AtomicLong以及Unsafe
聊聊并發(五)——原子操作的實現原理
AtomicInteger源碼分析——基于CAS的樂觀鎖實現
JAVA-CAS簡介以及ABA問題