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