一欺旧、從一個(gè)經(jīng)典的面試題談起
相信有過幾年的開發(fā)經(jīng)驗(yàn)的人或多或少對(duì)AtomicInteger類都會(huì)有些親切吧怯屉。AtomicInteger在java.util.concurrent.atomic包下,被稱為原子操作類。高并發(fā)的情況下训桶,我們可以在不使用鎖的前提下(如synchronized關(guān)鍵字不脯,或者Lock類)综苔,可以做到保證數(shù)據(jù)的正確性惩系。那他內(nèi)部是如何實(shí)現(xiàn)的呢?
在回答這個(gè)問題之前如筛,我們先看一個(gè)非常經(jīng)典的面試題堡牡,問我們?nèi)绾巫龅饺齻€(gè)線程循環(huán)打印ABC10次?其實(shí)這個(gè)解決思路有很多杨刨,可以使用最常見的鎖機(jī)制那一套(synchronized悴侵,notifyAll,wait),也可以使用Lock 和 Condition拭嫁,還可以使用Semaphore類可免,今天我們來說一種不使用鎖機(jī)制的思路。那就是下面要說的AtomicInteger類做粤。先看下具體的代碼實(shí)現(xiàn)浇借,他是如何實(shí)現(xiàn)三個(gè)線程循環(huán)打印ABC 10次的。
public class AtomicIntegerExample {
private AtomicInteger sycValue = new AtomicInteger(0);
private static final int MAX_SYC_VALUE = 3 * 10;
public static void main(String[] args) {
AtomicIntegerExample example = new AtomicIntegerExample();
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(example.new RunnableA());
service.execute(example.new RunnableB());
service.execute(example.new RunnableC());
service.shutdown();
}
private class RunnableA implements Runnable {
public void run() {
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 0) {
System.out.println(String.format("第%d遍",
sycValue.get() / 3 + 1));
System.out.println("A");
sycValue.getAndIncrement();
}
}
}
}
private class RunnableB implements Runnable {
public void run() {
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 1) {
System.out.println("B");
sycValue.getAndIncrement();
}
}
}
}
private class RunnableC implements Runnable {
public void run() {
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 2) {
System.out.println("C");
System.out.println();
sycValue.getAndIncrement();
}
}
}
}
}
代碼并不復(fù)雜怕品,我們看一下其中的一句關(guān)鍵代碼
sycValue.getAndIncrement();
上面的代碼執(zhí)行結(jié)果是可以正常依次循環(huán)打印ABC10次的妇垢。我們要分析他為什么可以實(shí)現(xiàn)上述需求。分析過程如下:
我們開啟了三個(gè)線程肉康,來分別打印ABC,我們暫且就叫ABC線程吧闯估。當(dāng)三個(gè)線程開始搶奪cpu執(zhí)行權(quán),我們假設(shè)C線程搶到了吼和,在run方法中我們可以看到
...
while (sycValue.get() < MAX_SYC_VALUE) {
if (sycValue.get() % 3 == 2) {
System.out.println("C");
System.out.println();
sycValue.getAndIncrement();
}
}
...
我們來看下其中關(guān)鍵代碼涨薪,getAndIncrement是AtomicInteger 類中的一個(gè)方法,我們進(jìn)入源碼中看看炫乓,這句代碼到底執(zhí)行了啥
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
這里有一個(gè)很意思的類Unsafe刚夺。要說到這個(gè)類就得牽扯出另外一個(gè)概念,也是今天要說的核心概念:CAS末捣。
二侠姑、CAS
CAS全稱是Compare And Swap翻譯過來就是比較交換。我們可以抽象出一個(gè)方法cas(V,E,N),其包含3個(gè)參數(shù)
V表示要更新的變量
E表示預(yù)期值
N表示新值
當(dāng)執(zhí)行cas函數(shù)時(shí)箩做,我們會(huì)比較V和E的值是否相等莽红,如果不相等,表示預(yù)期值和要更新的值不一樣說明該操作已經(jīng)有其他線程完成了邦邦,當(dāng)前線程無需做任何操作安吁。如果相等,則表示預(yù)期值和當(dāng)前要更新的值一樣圃酵,則說明此時(shí)是可以進(jìn)行cas操作的柳畔。
回到代碼這里,
利用Unsafe類的JNI本地方法實(shí)現(xiàn)郭赐,使用CAS指令薪韩,來保證讀-改-寫是一個(gè)原子操作。compareAndSwapInt有4個(gè)參數(shù)捌锭,this - 當(dāng)前AtomicInteger對(duì)象俘陷,valueOffset- value屬性在內(nèi)存中的位置(需要強(qiáng)調(diào)的不是value值在內(nèi)存中的位置),expect - 預(yù)期值观谦,update - 新值拉盾,根據(jù)上面的CAS操作過程,當(dāng)內(nèi)存中的value值等于expect值時(shí)豁状,則將內(nèi)存中的value值更新為update值捉偏,并返回true倒得,否則返回false。在這里我們有必要對(duì)Unsafe有一個(gè)簡(jiǎn)單點(diǎn)的認(rèn)識(shí)夭禽,從名字上來看霞掺,不安全,確實(shí)讹躯,這個(gè)類是用于執(zhí)行低級(jí)別的菩彬、不安全操作的方法集合,這個(gè)類中的方法大部分是對(duì)內(nèi)存的直接操作潮梯,所以不安全骗灶,但當(dāng)我們使用反射、并發(fā)包時(shí)秉馏,都間接的用到了Unsafe耙旦。
...
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
...
簡(jiǎn)單點(diǎn)理解,CAS保證數(shù)據(jù)操作的原子性沃饶。我們知道要使線程數(shù)據(jù)安全還得同時(shí)保證數(shù)據(jù)的可見性母廷,我們查看AtomicInteger源碼,會(huì)發(fā)現(xiàn)其實(shí)內(nèi)部是使用了volatile 關(guān)鍵字來保證數(shù)據(jù)的可見性糊肤。
...
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
...
三琴昆、AtomicInteger源碼閱讀總結(jié)
所以對(duì)AtomicInteger原子類操作為什么在高并發(fā)下能保證數(shù)據(jù)多而不亂,總結(jié)為:內(nèi)部使用cas指令保證了原子性馆揉,使用volatile關(guān)鍵字 保證了數(shù)據(jù)在線程中的可見性业舍。
四、數(shù)據(jù)庫樂觀鎖
既然談到了CAS操作方式升酣,那我們就談?wù)勁c之相關(guān)的數(shù)據(jù)庫樂觀鎖舷暮。為啥呢,因?yàn)閿?shù)據(jù)庫樂觀鎖實(shí)現(xiàn)方式之一就是使用CAS算法噩茄。
在談到樂觀鎖之前下面,我們先看下面一個(gè)案例。
現(xiàn)在有張賬戶表绩聘,有賬號(hào)id和余額money兩個(gè)字段沥割。一般用戶購(gòu)買商品,系統(tǒng)要做的流程一般如下:
1凿菩、查詢當(dāng)前賬號(hào)余額机杜;
2、根據(jù)余額與商品價(jià)格判斷用戶是否可以進(jìn)行下單購(gòu)買
3衅谷、余額可以支持用戶下單椒拗,下單之后對(duì)賬戶表中的余額值進(jìn)行相應(yīng)的扣除
假設(shè)有兩個(gè)人A、B同時(shí)使用同一個(gè)賬號(hào)id為123456的賬號(hào)購(gòu)買商品,已知該賬號(hào)還剩1000元蚀苛。當(dāng)A和B都看中了一款價(jià)格為800元的商品在验,他們同時(shí)進(jìn)行了下單。在第一步中枉阵,A和B同時(shí)下單,對(duì)于系統(tǒng)而言译红,執(zhí)行了以下sql
select * from table where id=123456
AB此時(shí)都得知賬戶有1000元,于是進(jìn)行第二步的時(shí)候判斷都是可以下單的兴溜。往下走,進(jìn)行第三步耻陕,更新賬戶表中的余額值拙徽。這下一個(gè)流程走下來,AB使用相同的賬號(hào)購(gòu)買了2件800的商品诗宣,結(jié)果賬戶余額還剩200.這樣的結(jié)果老板肯定是會(huì)虧的褲衩都不剩的膘怕。那怎么解決這個(gè)問題呢?我們可以使用數(shù)據(jù)庫鎖機(jī)制來解決召庞。
數(shù)據(jù)庫鎖分為悲觀鎖和樂觀鎖岛心。
悲觀鎖,通俗的理解總是假設(shè)最壞的情況篮灼,每次取數(shù)據(jù)時(shí)都認(rèn)為其他線程會(huì)修改忘古,所以都會(huì)加鎖(讀鎖、寫鎖诅诱、行鎖等)髓堪,當(dāng)其他線程想要訪問數(shù)據(jù)時(shí),都需要阻塞掛起娘荡「膳裕可以依靠數(shù)據(jù)庫實(shí)現(xiàn),如行鎖炮沐、讀鎖和寫鎖等争群,都是在操作之前加鎖,在Java中大年,synchronized的思想也是悲觀鎖换薄。
樂觀鎖,總是認(rèn)為不會(huì)產(chǎn)生并發(fā)問題鲜戒,每次去取數(shù)據(jù)的時(shí)候總認(rèn)為不會(huì)有其他線程對(duì)數(shù)據(jù)進(jìn)行修改专控,因此不會(huì)上鎖,但是在更新時(shí)會(huì)判斷其他線程在這之前有沒有對(duì)數(shù)據(jù)進(jìn)行修改遏餐。
樂觀鎖有兩種方式可以實(shí)現(xiàn)伦腐,
一種是我們剛剛上面提到的使用CAS操作,當(dāng)需要更新時(shí)失都,判斷當(dāng)前內(nèi)存值與之前取到的值是否相等柏蘑,若相等幸冻,則用新值更新,若失敗則重試咳焚,一般情況下是一個(gè)自旋操作洽损,即不斷的重試。注意:這種方式會(huì)產(chǎn)生ABA的問題革半。
還有一種方式碑定,是在數(shù)據(jù)表中加上一個(gè)數(shù)據(jù)版本號(hào)version字段,默認(rèn)為0又官,表示數(shù)據(jù)被修改的次數(shù)延刘,當(dāng)數(shù)據(jù)被修改時(shí),version值會(huì)加一六敬。當(dāng)線程A要更新數(shù)據(jù)值時(shí)碘赖,在讀取數(shù)據(jù)的同時(shí)也會(huì)讀取version值,在提交更新時(shí)外构,若剛才讀取到的version值為當(dāng)前數(shù)據(jù)庫中的version值相等時(shí)才更新普泡,否則重試更新操作,直到更新成功审编。如在該場(chǎng)景中撼班,我們?cè)谫~戶表中添加一個(gè)version字段,在第一步和第二步中和前面結(jié)果是一樣的割笙,當(dāng)進(jìn)行第三步的時(shí)候权烧,加入A先執(zhí)行了更新賬戶表余額操作,那么此時(shí)該賬戶的version為1伤溉,當(dāng)B執(zhí)行的時(shí)候般码,本質(zhì)是執(zhí)行以下sql
//因?yàn)樵诘谝徊讲樵兊臅r(shí)候,B得到的version值為0
update table set money =200 where id =123456 and version=0
很顯然此時(shí)B無法更新該賬戶的數(shù)據(jù)乱顾,因?yàn)榇藭r(shí)version已經(jīng)在A更新的時(shí)候增加了1板祝,此時(shí)version是1。這樣我們就可以通過update的記錄數(shù)得知其是否更新成功走净。