<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          悲觀鎖和樂觀鎖的那些事兒

          共 3823字,需瀏覽 8分鐘

           ·

          2021-01-04 16:54


          本文公眾號(hào)來源:鄙人薛某

          作者:很懶的程序員

          本文已收錄至我的GitHub






          線程安全

          線程安全是程序開發(fā)中非常需要我們注意的一環(huán),當(dāng)程序存在并發(fā)的可能時(shí),如果我們不做特殊的處理,很容易就出現(xiàn)數(shù)據(jù)不一致的情況。

          通常情況下,我們可以用加鎖的方式來保證線程安全,通過對共享資源 (也就是要讀取的數(shù)據(jù)) 的加上"隔離的鎖",使得多個(gè)線程執(zhí)行的時(shí)候也不會(huì)互相影響,而悲觀鎖樂觀鎖正是并發(fā)控制中較為常用的技術(shù)手段。

          樂觀鎖和悲觀鎖

          什么是悲觀鎖?什么是樂觀鎖?其實(shí)從字面上就可以區(qū)分出兩者的區(qū)別,通俗點(diǎn)說,

          悲觀鎖

          悲觀鎖就好像一個(gè)有迫害妄想癥的患者,總是假設(shè)最壞的情況,每次拿數(shù)據(jù)的時(shí)候都以為別人會(huì)修改,所以每次拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,直到整個(gè)數(shù)據(jù)處理過程結(jié)束,其他的線程如果要拿數(shù)據(jù)就必須等當(dāng)前的鎖被釋放后才能操作。

          使用案例

          悲觀鎖的使用場景并不少見,數(shù)據(jù)庫很多地方就用到了這種鎖機(jī)制,比如行鎖,表鎖,讀鎖,寫鎖等,都是在做操作之前先上鎖,悲觀鎖的實(shí)現(xiàn)往往依靠數(shù)據(jù)庫本身的鎖功能實(shí)現(xiàn)。Java程序中的Synchronized和ReentrantLock等實(shí)現(xiàn)的鎖也均為悲觀鎖。

          在數(shù)據(jù)庫中,悲觀鎖的調(diào)用一般是在所要查詢的語句后面加上 for update

          select * from db_stock where goods_id = 1 for update

          當(dāng)有一個(gè)事務(wù)調(diào)用這條 sql 語句時(shí),會(huì)對goods_id = 1 這條記錄加鎖,其他的事務(wù)如果也對這條記錄做 for update 的查詢的話,那就必須等到該事務(wù)執(zhí)行完后才能查出結(jié)果,這種加鎖方式能對讀和寫做出排他的作用,保證了數(shù)據(jù)只能被當(dāng)前事務(wù)修改。

          當(dāng)然,如果其他事務(wù)只是簡單的查詢而沒有用 for update的話,那么查詢還是不會(huì)受影響的,只是說更新時(shí)一樣要等待當(dāng)前事務(wù)結(jié)束才行。

          值得注意的是,MySQL默認(rèn)使用autocommit模式,也就是說,當(dāng)你執(zhí)行一個(gè)更新操作后,MySQL會(huì)立刻將結(jié)果進(jìn)行提交,就是說,如果我們不僅要讀,還要更新數(shù)據(jù)的話,需要手動(dòng)控制事務(wù)的提交,比如像下面這樣:

          set autocommit=0;//開始事務(wù)begin;//查詢出商品id為1的庫存表數(shù)據(jù)select * from db_stock where goods_id = 1 for update;//減庫存update db_stock set stock_num = stock_num - 1 where goods_id = 1 ;//提交事務(wù)commit;

          雖然悲觀鎖能有效保證數(shù)據(jù)執(zhí)行的順序性和一致性,但在高并發(fā)場景下并不適用,試想,如果一個(gè)事務(wù)用悲觀鎖對數(shù)據(jù)加鎖之后,其他事務(wù)將不能對加鎖的數(shù)據(jù)進(jìn)行除了查詢以外的所有操作,如果該事務(wù)執(zhí)行時(shí)間很長,那么其他事務(wù)將一直等待,這無疑會(huì)降低系統(tǒng)的吞吐量。

          這種情況下,我們可以有更好的選擇,那就是樂觀鎖。

          樂觀鎖

          樂觀鎖的思想和悲觀鎖相反,總是假設(shè)最好的情況,認(rèn)為別人都是友好的,所以每次獲取數(shù)據(jù)的時(shí)候不會(huì)上鎖,但更新數(shù)據(jù)那一刻會(huì)判斷數(shù)據(jù)是否被更新過了,如果數(shù)據(jù)的值跟自己預(yù)期一樣的話,那么就可以正常更新數(shù)據(jù)。

          場景

          這種思想應(yīng)用到實(shí)際場景的話,可以用版本號(hào)機(jī)制和CAS算法實(shí)現(xiàn)。

          CAS

          CAS是一種無鎖的思想,它假設(shè)線程對資源的訪問是沒有沖突的,同時(shí)所有的線程執(zhí)行都不需要等待,可以持續(xù)執(zhí)行。如果遇到?jīng)_突的話,就使用一種叫做CAS (比較交換) 的技術(shù)來鑒別線程沖突,如果檢測到?jīng)_突發(fā)生,就重試當(dāng)前操作到?jīng)]有沖突為止。

          原理

          CAS的全稱是Compare-and-Swap,也就是比較并交換,它包含了三個(gè)參數(shù):V,A,B,V表示要讀寫的內(nèi)存位置,A表示舊的預(yù)期值,B表示新值

          具體的機(jī)制是,當(dāng)執(zhí)行CAS指令的時(shí)候,只有當(dāng)V的值等于預(yù)期值A(chǔ)時(shí),才會(huì)把V的值改為B,如果V和A不同,有可能是其他的線程修改了,這個(gè)時(shí)候,執(zhí)行CAS的線程就會(huì)不斷的循環(huán)重試,直到能成功更新為止。

          正是基于這樣的原理,CAS即時(shí)沒有使用鎖,也能發(fā)現(xiàn)其他線程對當(dāng)前線程的干擾,從而進(jìn)行及時(shí)的處理。

          缺點(diǎn)

          CAS算是比較高效的并發(fā)控制手段,不會(huì)阻塞其他線程。但是,這樣的更新方式是存在問題的,看流程就知道了,如果C的結(jié)果一直跟預(yù)期的結(jié)果不一樣的話,線程A就會(huì)一直不斷的循環(huán)重試,重試次數(shù)太多的話對CPU也是一筆不小的開銷。

          而且,CAS的操作范圍也比較局限,只能保證一個(gè)共享變量的原子操作,如果需要一段代碼塊的原子性的話,就只能通過Synchronized等工具來實(shí)現(xiàn)了。

          除此之外,CAS機(jī)制最大的缺陷就是"ABA"問題。

          ABA問題

          前面說過,CAS判斷變量操作成功的條件是V的值和A是一致的,這個(gè)邏輯有個(gè)小小的缺陷,就是如果V的值一開始為A,在準(zhǔn)備修改為新值前的期間曾經(jīng)被改成了B,后來又被改回為A,經(jīng)過兩次的線程修改對象的值還是舊值,那么CAS操作就會(huì)誤任務(wù)該變量從來沒被修改過,這就是CAS中的“ABA”問題。

          看完流程圖相信也不用我說太多了吧,線程多發(fā)的情況下,這樣的問題是非常有可能發(fā)生的,那么如何避免ABA問題呢?

          加標(biāo)志位,例如搞個(gè)自增的字段,沒操作一次就加一,或者是一個(gè)時(shí)間戳,每次更新比較時(shí)間戳的值,這也是數(shù)據(jù)庫版本號(hào)更新的思想(下面會(huì)說到)

          在Java中,自JDK1.5以后就提供了這么一個(gè)并發(fā)工具類AtomicStampedReference,該工具內(nèi)部維護(hù)了一個(gè)內(nèi)部類,在原有基礎(chǔ)上維護(hù)了一個(gè)對象,及一個(gè)int類型的值(可以理解為版本號(hào)),在每次進(jìn)行對比修改時(shí),都會(huì)先判斷要修改的值,和內(nèi)存中的值是否相同,以及版本號(hào)是否相同,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。

          private static class Pair {        final T reference;        final int stamp;        private Pair(T reference, int stamp) {            this.reference = reference;            this.stamp = stamp;        }        static  Pair of(T reference, int stamp) {            return new Pair(reference, stamp);        }    }

          適用場景

          CAS一般適用于讀多寫少的場景,因?yàn)檫@種情況線程的沖突不會(huì)太多,也只有線程沖突不嚴(yán)重的情況下,CAS的線程循環(huán)次數(shù)才能有效的降低,性能也能更高。

          版本號(hào)機(jī)制

          版本號(hào)機(jī)制是數(shù)據(jù)庫更新操作里非常實(shí)用的技巧,其實(shí)原理很簡單,就是獲取數(shù)據(jù)的時(shí)候會(huì)拿一個(gè)能對應(yīng)版本的字段,然后更新的時(shí)候判斷這個(gè)字段是否跟之前拿的值是否一致,一致的話證明數(shù)據(jù)沒有被別人更新過,這時(shí)就可以正常實(shí)現(xiàn)更新操作。

          還是上面的那張表為例,我們加上一個(gè)版本號(hào)字段version,然后每次更新數(shù)據(jù)的時(shí)候就把版本號(hào)加1,

          select goods_id,stock_num,version from db_stock where goods_id = 1
          update db_stock set stock_num = stock_num - 1,version = version + 1 where goods_id = 1 and version = #{version}

          這樣的話,如果有兩個(gè)事務(wù)同時(shí)對goods_id = 1這條數(shù)據(jù)做更新操作的話,一定會(huì)有一個(gè)事務(wù)先執(zhí)行完成,然后version字段就加1,另一個(gè)事務(wù)更新的時(shí)候發(fā)現(xiàn)version已經(jīng)不是之前獲取到的那個(gè)值了,就會(huì)重新執(zhí)行查詢操作,從而保證了數(shù)據(jù)的一致性。

          這種鎖的方式也不會(huì)影響吞吐量,畢竟大家都可以同時(shí)讀和寫,但高并發(fā)場景下,sql更新報(bào)錯(cuò)的可能性會(huì)大大增加,這樣對業(yè)務(wù)處理似乎也不友好。

          這種情況下,我們可以把鎖的粒度縮小,比如說減庫存的時(shí)候,我們可以這么處理:

          update db_stock set stock_num = stock_num - 1  where goods_id = 1 and stock_num > 0

          這樣一來,sql更新沖突的概率會(huì)大大降低,而且也不用去單獨(dú)維護(hù)類似version的字段了。

          最后

          關(guān)于悲觀鎖和樂觀鎖的例子介紹就到這兒了,當(dāng)然,本文也只是略微講解,更多的知識(shí)點(diǎn)還要靠大家研究,而且,除了這兩種鎖,并發(fā)控制中還有很多其他的控制手段,像什么Synchronized、ReentrantLock、公平鎖,非公平鎖之類的都是很常見的并發(fā)知識(shí),不管是為了日常開發(fā)還是應(yīng)付面試,掌握這些知識(shí)點(diǎn)還是很有必要的,而且,并發(fā)編程的知識(shí)思想是共通的,知道一塊知識(shí)點(diǎn)后很容易就能延伸去學(xué)習(xí)其他的知識(shí)點(diǎn)。

          歡迎關(guān)注我的微信公眾號(hào)【面試造火箭】來聊聊Java面試

          添加我的微信進(jìn)一步交流和學(xué)習(xí)

          如果顯示頻繁,微信手動(dòng)搜索sanwaiyihao添加即可

          點(diǎn)亮在看轉(zhuǎn)發(fā)是我持續(xù)更新的動(dòng)力,對我真的很重要!

          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  18禁成人网站 | 无码人妻精品一区二区 | www.视频一区 | 操高中生到高潮在线观看免费 | 色色综合五月天 |