什么是Atomic
Atomic是原子性的意思,可以自動更新,用于原子增量計數(shù)器之類的應用程序??梢越鉀Q多線程環(huán)境遞增的異議性問題。
怎么使用Atomic
AtomicIntegerDemo
public class Atomic {
AtomicInteger integer = new AtomicInteger(0);
@Test
public void testAtomicInteger() throws InterruptedException {
ExecutorService executor = new ThreadPoolExecutor(10, 50, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
for (int i = 0; i < 100; i++) {
executor.execute(this::add);
TimeUnit.MILLISECONDS.sleep(1);
}
executor.shutdown();
}
public void add() {
for (int i = 0; i < 100; i++) {
System.out.println(integer.incrementAndGet());
}
}
}
運行結果

為什么要使用Atomic類
首先看一下不使用Atomic,以上Demo的運行結果會有什么問題
不使用AtomicDemo
public class IntDemo {
int a = 1;
@Test
public void testInt() {
final int[] int1 = {0};
ExecutorService executor = new ThreadPoolExecutor(10, 50, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
for (int j = 0; j < 10; j++) {
int1[0]++;
System.out.println(int1[0]);
}
});
}
executor.shutdown();
}
}
運行結果

從運行結果來看,最終的結果不為100,可見在多線程的環(huán)境下,int自增操作并不是原子性的,這樣就會導致一些問題。
為什么會出現(xiàn)這個問題?
1. 首先先了解一下,底層CPU、緩存已經主存之間的關系(intel)

- 主存(Main Memory):允許application,Java代碼將會編譯成字節(jié)碼,然后由操作系統(tǒng)翻譯成機器碼,最后加載到內存中。
- L3 unified cache:L3級緩存,這一塊的數(shù)據(jù)的是被封裝的CPU的所有核心共享的,也是三級緩存中容量最大。
- L2 unified cache:L2級緩存,這一塊的數(shù)據(jù)是被單個核心所獨享的。
- L1 unified cache:L2級緩存,這一塊的數(shù)據(jù)是被單個核心所獨享的。
- ALU:CPU計算單元,負責數(shù)理邏輯計算。
- Register:寄存器單元,其中包含若干的寄存器,有PC(程序計數(shù)器)、IR(指令寄存器)、DR(數(shù)據(jù)寄存器)等。
以上程序的共享變量i,沒有進行鎖、同步等數(shù)據(jù)一致性處理。變量i會被從內存讀取到CPU封裝的L3緩存,如果在多線程環(huán)境下,會存在兩個操作變量i的線程同時跑在不同的核心上。
假設線程A->Core1,線程B->Core2,線程A從L3中讀取到變量i為0,線程B從L3中讀取到變量i也為0,線程A對變量i++得到i=1,同樣線程B也對變量i++得到i=1,回寫到內存時i=1,但是實際上已經進行了兩次的++,故結果不正確。
特殊:四核八線程,對于一個核心,為了提高ALU的計算效率,會存在一個ALU單元對應兩組Register,也就是所謂的超線程。此處的數(shù)據(jù)同步問題,博主還在學習?!救绻写罄辛私猓梢砸黄鹧芯垦芯俊?/p>
2. 再來了解一下,JMM(Java Memory Model)

- 線程A從主存中將變量讀取到本地內存,僅僅是讀取,后續(xù)還要進行加載。
- 將讀取到的變量加載到本地工作內存,此時變量是主存中變量的副本。
- 將線程本地變量讀取到執(zhí)行引擎進行計算。
- 將第3步的計算結果刷回到線程本地工作內存中。
- 將本地工作內存寫入到主存中,僅僅是寫操作,后續(xù)還要進行存儲。
- 將線程本地計算結果寫回主存中。
- 線程B和線程B可以同時進行以上6步。
從以上的JMM模型的執(zhí)行流程來看,當多線程的環(huán)境下,線程A和線程B可以同時讀取主存中的變量,然后復制到本地工作內存中,接著計算,最后在將計算結果寫回到主存中會存在數(shù)據(jù)不一致性。
Atomic原子類是如何保證并發(fā)環(huán)境數(shù)據(jù)一致性的?
上源碼
在前文中,對于AtomicInteger遞增是調用的incrementAndGet

從源碼中可見,調用的是unsafe.getAndAddInt,讓我們來看看這個方法的實現(xiàn)。

從源碼中可見,先是以getIntVolatile的方法(native方法)獲取變量的值,然后調用compareAndSwapInt的方法(著名的CAS)進行數(shù)據(jù)的更改操作。
CAS原理(類似于一種樂觀鎖的概念)
CAS(compare and swap),比較并交換。

- 在并發(fā)環(huán)境下
- 讀?。?
- 每個修改共享變量的線程都可以讀取并進行修改
- 寫入:
- 如果此時的數(shù)據(jù)等于該線程一開始讀取到的值,則將計算結果寫入到主存中,
- 否則就重新讀取最新值,然后進行重新計算,反復如此操作,直到寫入成功。
- 針對于其中的ABA問題,可以使用一個version來解決,version可以是uuid或者時間戳等,具體可以取決于業(yè)務場景。
深入源碼(以AtomicInteger為例)
類關系圖

如圖,可知AtomicInteger繼承了Number抽象類,此抽象類中定義了一些關于數(shù)字之間的一些基礎操作,具體方法如下圖。

成員變量

構造方法&set&get

- 構造方法有兩個:一個無參構造器、一個有參構造器
-
get:獲取value -
set:設置value -
lazySet:異步設置value
加/減/設值操作


-
getAndSet(int newValue):將變量值設置成newValue,并返回舊值。 -
compareAndSet(int expect, int update):比較并設置值,只有當原有的value=expect時,才會將變量值設置成update,返回操作結果。 -
weakCompareAndSet:與compareAndSet(int expect, int update)類似,但是不強制原子性。 -
getAndIncrement():原子遞增,返回舊值。 -
getAndDecrement():原子遞減,返回舊值。 -
getAndAdd(int delta):原子增加delta,返回舊值。 -
incrementAndGet():原子遞增,返回新值。 -
decrementAndGet():原子遞減,返回新值。 -
addAndGet(int delta):原子增加delta,返回新值。
更新操作
getAndUpdate(IntUnaryOperator updateFunction)

此方法會先獲取之前的值,然后將updateFunction函數(shù)作用于之前的讀取出來的值,最后將變量設置成計算得到的結果。此方法返回操作之前的舊值。
updateAndGet(IntUnaryOperator updateFunction)

此方法會先獲取之前的值,然后將updateFunction函數(shù)作用于之前的讀取出來的值,最后將變量設置成計算得到的結果。此方法返回操作之后的新值。
累加操作
getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)

此方法會先獲取之前的值,然后將accumulatorFunction函數(shù)作用于之前的讀取出來的值(其實就是將之前的舊值+x),最后將變量設置成計算得到的結果。此方法返回操作之前的舊值。
accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)

此方法會先獲取之前的值,然后將accumulatorFunction函數(shù)作用于之前的讀取出來的值(其實就是將之前的舊值+x),最后將變量設置成計算得到的結果。此方法返回操作之后的新值。
總結:對于get在方法名稱前面的話,那么會返回操作之前的舊值。如果子啊方法名稱后面,那么會返回操作之后的新值