目錄
一、背景
二、解決方法
三、原子操作CAS
四、CAS的缺點
4.1 ABA問題
4.2 回圈開銷大
4.3 只能保證一個共享變數的原子操作
五、原子操作類
六、demo
一、背景
我們都知道在多執行緒環境下,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());
}
}

---------------------------------------------------------------------------------------------------------------------------------------------------
如果我的文章對您有點幫助,麻煩點個贊,您的鼓勵將是我繼續寫作的動力
如果哪里有不正確的地方,歡迎指正
如果哪里表述不清,歡迎留言討論
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/242834.html
標籤:其他
上一篇:論文推薦丨DBOS: 一個以資料為中心的作業系統的建議
下一篇:h5打開小程式,h5跳轉到小程式
