由AtomicInteger談到數(shù)據(jù)庫樂觀鎖

一欺旧、從一個(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ù)得知其是否更新成功走净。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末券时,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子伏伯,更是在濱河造成了極大的恐慌橘洞,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件说搅,死亡現(xiàn)場(chǎng)離奇詭異炸枣,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門适肠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霍衫,“玉大人,你說我怎么就攤上這事侯养《氐” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵逛揩,是天一觀的道長(zhǎng)柠傍。 經(jīng)常有香客問我,道長(zhǎng)息尺,這世上最難降的妖魔是什么携兵? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮搂誉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘静檬。我一直安慰自己炭懊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布拂檩。 她就那樣靜靜地躺著侮腹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪稻励。 梳的紋絲不亂的頭發(fā)上父阻,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音望抽,去河邊找鬼加矛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛煤篙,可吹牛的內(nèi)容都是我干的斟览。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辑奈,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼苛茂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸠窗,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤妓羊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后稍计,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躁绸,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涨颜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片费韭。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖庭瑰,靈堂內(nèi)的尸體忽然破棺而出星持,到底是詐尸還是另有隱情,我是刑警寧澤弹灭,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布督暂,位于F島的核電站,受9級(jí)特大地震影響穷吮,放射性物質(zhì)發(fā)生泄漏逻翁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一捡鱼、第九天 我趴在偏房一處隱蔽的房頂上張望八回。 院中可真熱鬧,春花似錦驾诈、人聲如沸缠诅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽管引。三九已至,卻和暖如春闯两,著一層夾襖步出監(jiān)牢的瞬間褥伴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工漾狼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留重慢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓邦投,卻偏偏與公主長(zhǎng)得像伤锚,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子志衣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容