目錄
一、背景
我們都知道在多線程環境下,num++,這個操作是不安全的,因爲它不是原子操作
在底層,這個加1的操作會被分成幾個步驟:
1、從內存中讀取 num
2、然後執行 num + 1
3、然後把新值寫入內存
直接看代碼
public class TestInt {
private volatile int num = 0;
public int getNum() {
return this.num;
}
public void increase() {
this.num++;
}
public static void main(String[] args) {
final TestInt testInt = new TestInt();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
testInt.increase();
}
}).start();
}
// 讓所有子線程執行完畢
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num:" + testInt.getNum());
}
}
按照我們想的,num結果應該爲100,實際測試時,結果卻每次都不一樣(小於或者等於100)
比如說有下面這種情況:
線程A獲取到num的值 = 0,由於它不是原子性,cpu資源被線程B搶走(或者A的時間片執行時間已到)
線程B獲取num的值 = 0
線程B對num + 1 = 1
線程B把num值更新到主內存中後結束
線程A重新獲得cpu資源, 對它內存中num的副本 + 1 = 1
線程A將num = 1更新到主內存中
本來應該num應該爲2的,結果卻爲1
二、解決方法
爲了保證線程的安全,我們第一步就想到使用synchronized關鍵字加鎖,這樣肯定可以解決問題
但是鎖機制也不是在任何情況下都是最優選擇
synchronized是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖
這樣可能會造成以下問題:
1、被阻塞的線程優先級很高
2、獲得鎖的線程一直不釋放鎖
3、大量的線程來競爭鎖,導致CPU資源的浪費
4、如果只是一個計數器,使用鎖機制比較笨重
三、原子操作CAS
原子操作定義:假定有兩個任務A和B,如果從執行A的線程來看,當另一個線程執行B時,要麼將B全部執行完,要麼完全不執行B,那麼A和B對彼此來說是原子的
那麼CAS是如何做到原子操作的呢?
它是利用了現代處理器都支持的CAS指令,循環這個指令,直到成功爲止
也就是說,它不是通過語言級別的操作來保證原子操作,而是在更底層,CPU指令級別的操作保證了原子操作
CAS操作過程都包含三個運算符:一個內存地址V,一個期望的值A和一個新值B
如果這個地址V上存放的值(也就是內存中的值)等於這個期望的值A,則將地址上的值賦爲新值B
上述動作是在一個循環中進行(for(;;){},也稱爲自旋操作,其實就是一個死循環),直到修改成功
我們可以先看一下CAS實現類AtomicInteger中如何實現類似++i的源碼
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
舉個例子
A,B兩個線程同時修改變量num的值0,進行加1操作
1、獲取值。首先他們都會去內存中獲取num=0,拷貝到自己工作空間,此時他們的期望值都是0
2、計算新值
3、比較和交換。內存中的值與期望值相等,則交換。這裏通過CAS指令,保證了比較和交換是一個原子操作
並且要注意:該值是volatile修飾,保證了值修改時,其他線程可以立馬知道
A和B都執行完第2步,第3步只有一個線程可以成功執行
這裏爲了好理解,假設A快一點,先執行第3步,先比較期望值0和內存中值是否相等,發現相等,把新值1刷回內存中,然後返回結束循環
B慢一點,比較期望值0和內存中值(此時已變爲1,因爲是volatile修飾)是否相等,不相等,則執行下一次循環
再獲取內存中的值1拷貝到自己工作空間,也就是期望值爲1
再計算新值爲2
再比較和交換,這時相等,就把新值刷回內存,然後返回結束循環
四、CAS的缺點
4.1 ABA問題
從上面介紹,CAS操作經過幾個步驟,獲取值,比較,修改
如果在獲取值和比較之間,該值從原有的A,變爲B,再變爲A,CAS是不知道該值發生了變化
所以使用了版本號來解決該問題,每次變量更新都會把版本號加1,此時A→B→A就會變成1A→2B→3A
4.2 循環開銷大
自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷
4.3 只能保證一個共享變量的原子操作
當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作
但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就需要用鎖機制
但是,我們可以把多個共享變量合併成一個變量,來進行CAS操作
五、原子操作類
java在java.util.concurrent.atomic包下,爲我們提供了一系列以Atomic開頭的包裝類,方便我們使用
jdk1.5的atomic包下提供的原子操作類
標量類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器類:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
複合變量類:AtomicMarkableReference,AtomicStampedReference
jdk1.8之後又添加了下面的四個類
LongAdder DoubleAdder 高併發情況下替代AtomicLong
LongAccumulator DoubleAccumulator
六、demo
以AtomicInteger爲例,提供了getAndIncrement()(類似i++操作)、incrementAndGet()(類似++i操作)、get()等方法
public class TestAtomicInt {
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
System.out.println(num.getAndIncrement());
System.out.println(num.incrementAndGet());
System.out.println(num.get());
}
}
---------------------------------------------------------------------------------------------------------------------------------------------------
如果我的文章對您有點幫助,麻煩點個贊,您的鼓勵將是我繼續寫作的動力
如果哪裏有不正確的地方,歡迎指正
如果哪裏表述不清,歡迎留言討論