目錄html
Java 按照鎖的實現分爲樂觀鎖和悲觀鎖,樂觀鎖和悲觀鎖並非一種真實存在的鎖,而是一種設計思想,樂觀鎖和悲觀鎖對於理解 Java 多線程和數據庫來講相當重要,那麼本篇文章就來詳細探討一下這兩種鎖的概念以及實現方式。java
悲觀鎖
是一種悲觀思想,它總認爲最壞的狀況可能會出現,它認爲數據極可能會被其餘人所修改,因此悲觀鎖在持有數據的時候總會把資源
或者 數據
鎖住,這樣其餘線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放爲止。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。悲觀鎖的實現每每依靠數據庫自己的鎖功能實現。mysql
Java 中的 Synchronized
和 ReentrantLock
等獨佔鎖(排他鎖)也是一種悲觀鎖思想的實現,由於 Synchronzied 和 ReetrantLock 不論是否持有資源,它都會嘗試去加鎖,生怕本身心愛的寶貝被別人拿走。算法
樂觀鎖的思想與悲觀鎖的思想相反,它總認爲資源和數據不會被別人所修改,因此讀取不會上鎖,可是樂觀鎖在進行寫入操做的時候會判斷當前數據是否被修改過(具體如何判斷咱們下面再說)。樂觀鎖的實現方案通常來講有兩種: 版本號機制
和 CAS實現
。樂觀鎖多適用於多度的應用類型,這樣能夠提升吞吐量。sql
在Java中java.util.concurrent.atomic
包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。數據庫
上面介紹了兩種鎖的基本概念,並提到了兩種鎖的適用場景,通常來講,悲觀鎖不只會對寫操做加鎖還會對讀操做加鎖,一個典型的悲觀鎖調用:編程
select * from student where name="cxuan" for update
這條 sql 語句從 Student 表中選取 name = "cxuan" 的記錄並對其加鎖,那麼其餘寫操做再這個事務提交以前都不會對這條數據進行操做,起到了獨佔和排他的做用。安全
悲觀鎖由於對讀寫都加鎖,因此它的性能比較低,對於如今互聯網提倡的三高
(高性能、高可用、高併發)來講,悲觀鎖的實現用的愈來愈少了,可是通常多讀的狀況下仍是須要使用悲觀鎖的,由於雖然加鎖的性能比較低,可是也阻止了像樂觀鎖同樣,遇到寫不一致的狀況下一直重試的時間。多線程
相對而言,樂觀鎖用於讀多寫少的狀況,即不多發生衝突的場景,這樣能夠省去鎖的開銷,增長系統的吞吐量。併發
樂觀鎖的適用場景有不少,典型的好比說成本系統,櫃員要對一筆金額作修改,爲了保證數據的準確性和實效性,使用悲觀鎖鎖住某個數據後,再遇到其餘須要修改數據的操做,那麼此操做就沒法完成金額的修改,對產品來講是災難性的一刻,使用樂觀鎖的版本號機制可以解決這個問題,咱們下面說。
樂觀鎖通常有兩種實現方式:採用版本號機制
和 CAS(Compare-and-Swap,即比較並替換)算法
實現。
版本號機制是在數據表中加上一個 version
字段來實現的,表示數據被修改的次數,當執行寫操做而且寫入成功後,version = version + 1,當線程A要更新數據時,在讀取數據的同時也會讀取 version 值,在提交更新時,若剛纔讀取到的 version 值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。
咱們以上面的金融系統爲例,來簡述一下這個過程。
金額
和 version
,金額的屬性是可以實時變化,而 version 表示的是金額每次發生變化的版本,通常的策略是,當金額發生改變時,version 採用遞增的策略每次都在上一個版本號的基礎上 + 1。上面兩種狀況是最樂觀的狀況,上面的兩個事務都是順序執行的,也就是事務一和事務二互不干擾,那麼事務要並行執行會如何呢?
事務一開啓,男櫃員先執行讀操做,取出金額和版本號,執行寫操做
begin update 表 set 金額 = 120,version = version + 1 where 金額 = 100 and version = 0
此時金額改成 120,版本號爲1,事務尚未提交
事務二開啓,女櫃員先執行讀操做,取出金額和版本號,執行寫操做
begin update 表 set 金額 = 50,version = version + 1 where 金額 = 100 and version = 0
此時金額改成 50,版本號變爲 1,事務未提交
如今提交事務一,金額改成 120,版本變爲1,提交事務。理想狀況下應該變爲 金額 = 50,版本號 = 2,可是實際上事務二 的更新是創建在金額爲 100 和 版本號爲 0 的基礎上的,因此事務二不會提交成功,應該從新讀取金額和版本號,再次進行寫操做。
這樣,就避免了女櫃員 用基於 version=0 的舊數據修改的結果覆蓋男操做員操做結果的可能。
先來看一道經典的併發執行 1000次遞增和遞減後的問題:
public class Counter { int count = 0; public int getCount() { return count; } public void setCount(int count) { this.count = count; } public void add(){ count += 1; } public void dec(){ count -= 1; } }
public class Consumer extends Thread{ Counter counter; public Consumer(Counter counter){ this.counter = counter; } @Override public void run() { for(int j = 0;j < Test.LOOP;j++){ counter.dec(); } } } public class Producer extends Thread{ Counter counter; public Producer(Counter counter){ this.counter = counter; } @Override public void run() { for(int i = 0;i < Test.LOOP;++i){ counter.add(); } } } public class Test { final static int LOOP = 1000; public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Producer producer = new Producer(counter); Consumer consumer = new Consumer(counter); producer.start(); consumer.start(); producer.join(); consumer.join(); System.out.println(counter.getCount()); } }
屢次測試的結果都不爲 0,也就是說出現了併發後數據不一致的問題,緣由是 count -= 1 和 count += 1 都是非原子性操做,它們的執行步驟分爲三步:
若是要把證它們的原子性,必須進行加鎖,使用 Synchronzied
或者 ReentrantLock
,咱們前面介紹它們是悲觀鎖的實現,咱們如今討論的是樂觀鎖,那麼用哪一種方式保證它們的原子性呢?請繼續往下看
CAS 即 compare and swap(比較與交換)
,是一種有名的無鎖算法。即不使用鎖的狀況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的狀況下實現變量的同步,因此也叫非阻塞同步(Non-blocking Synchronization
CAS 中涉及三個要素:
當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
JAVA對CAS的支持:在JDK1.5 中新添加 java.util.concurrent (J.U.C) 就是創建在 CAS 之上的。對於 synchronized 這種阻塞算法,CAS是非阻塞算法的一種實現。因此J.U.C在性能上有了很大的提高。
咱們以 java.util.concurrent 中的AtomicInteger
爲例,看一下在不用鎖的狀況下是如何保證線程安全的
public class AtomicCounter { private AtomicInteger integer = new AtomicInteger(); public AtomicInteger getInteger() { return integer; } public void setInteger(AtomicInteger integer) { this.integer = integer; } public void increment(){ integer.incrementAndGet(); } public void decrement(){ integer.decrementAndGet(); } } public class AtomicProducer extends Thread{ private AtomicCounter atomicCounter; public AtomicProducer(AtomicCounter atomicCounter){ this.atomicCounter = atomicCounter; } @Override public void run() { for(int j = 0; j < AtomicTest.LOOP; j++) { System.out.println("producer : " + atomicCounter.getInteger()); atomicCounter.increment(); } } } public class AtomicConsumer extends Thread{ private AtomicCounter atomicCounter; public AtomicConsumer(AtomicCounter atomicCounter){ this.atomicCounter = atomicCounter; } @Override public void run() { for(int j = 0; j < AtomicTest.LOOP; j++) { System.out.println("consumer : " + atomicCounter.getInteger()); atomicCounter.decrement(); } } } public class AtomicTest { final static int LOOP = 10000; public static void main(String[] args) throws InterruptedException { AtomicCounter counter = new AtomicCounter(); AtomicProducer producer = new AtomicProducer(counter); AtomicConsumer consumer = new AtomicConsumer(counter); producer.start(); consumer.start(); producer.join(); consumer.join(); System.out.println(counter.getInteger()); } }
經測試可得,無論循環多少次最後的結果都是0,也就是多線程並行的狀況下,使用 AtomicInteger 能夠保證線程安全性。 incrementAndGet 和 decrementAndGet 都是原子性操做。本篇文章暫不探討它們的實現方式。
任何事情都是有利也有弊,軟件行業沒有完美的解決方案只有最優的解決方案,因此樂觀鎖也有它的弱點和缺陷:
ABA 問題說的是,若是一個變量第一次讀取的值是 A,準備好須要對 A 進行寫操做的時候,發現值仍是 A,那麼這種狀況下,能認爲 A 的值沒有被改變過嗎?能夠是由 A -> B -> A 的這種狀況,可是 AtomicInteger 卻不會這麼認爲,它只相信它看到的,它看到的是什麼就是什麼。
JDK 1.5 之後的 AtomicStampedReference
類就提供了此種能力,其中的 compareAndSet 方法
就是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
也能夠採用CAS的一個變種DCAS來解決這個問題。
DCAS,是對於每個V增長一個引用的表示修改次數的標記符。對於每一個V,若是引用修改了一次,這個計數器就加1。而後再這個變量須要update的時候,就同時檢查變量的值和計數器的值。
咱們知道樂觀鎖在進行寫操做的時候會判斷是否可以寫入成功,若是寫入不成功將觸發等待 -> 重試機制,這種狀況是一個自旋鎖,簡單來講就是適用於短時間內獲取不到,進行等待重試的鎖,它不適用於長期獲取不到鎖的狀況,另外,自旋循環對於性能開銷比較大。
簡單的來講 CAS 適用於寫比較少的狀況下(多讀場景,衝突通常較少),synchronized 適用於寫比較多的狀況下(多寫場景,衝突通常較多)
補充: Java併發編程這個領域中 synchronized 關鍵字一直都是元老級的角色,好久以前不少人都會稱它爲 「重量級鎖」 。可是,在JavaSE 1.6以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各類優化以後變得在某些狀況下並非那麼重了。synchronized 的底層實現主要依靠 Lock-Free 的隊列,基本思路是 自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但得到了高吞吐量。在線程衝突較少的狀況下,能夠得到和 CAS 相似的性能;而線程衝突嚴重的狀況下,性能遠高於CAS。
歡迎關注我本人的公衆號,公號回覆002有你想要的一切
相關參考:
https://baike.baidu.com/item/悲觀鎖