闭关修炼(六)各种锁
難者不會,會者不難
文章目錄
- java中的鎖你聽過哪些?
- 悲觀鎖
- 什么是悲觀鎖?
- 悲觀鎖的缺點
- 樂觀鎖
- 什么是樂觀鎖?
- 悲觀鎖和樂觀鎖的區別?
- 使用場景的區別?
- 重入鎖
- 什么現象非重入鎖會產生而重入鎖不會產生?
- 重入鎖有哪些?
- 什么是重入鎖
- 可重入鎖-synchronized例子
- 可重入鎖-ReentrantLock例子
- 重入鎖的好處?
- 注意的小點
- 讀寫鎖
- 讀寫鎖的機制?
- 例子
- CAS無鎖機制
- CAS是什么意思?
- 什么是CAS無鎖機制?
- 例子
- 自旋鎖
- 什么是自旋鎖
- 例子
- 分布式鎖
java中的鎖你聽過哪些?
悲觀鎖,樂觀鎖,分段鎖,重入鎖,讀寫鎖,CAS鎖,排他鎖(基本上是自帶的功能),自旋鎖,分布式鎖(muji)
悲觀鎖
什么是悲觀鎖?
老生常談的問題。
悲觀鎖每次在拿數據時,都會上鎖,自帶排他鎖的功能。
舉個具體的例子,有張Order表,里有id,orderId,state屬性,另外張Money表中,里有id,orderId,money屬性,現在有個業務執行三條sql語句:
select * from order where orderId=1 and state=0,如果select到內容繼續往下執行:
update set state=1 where orderId=1,
update money set money=moeny+money where orderId=1
此時有兩個jdbc連接,同時執行以上三條sql語句,會發生什么?產生重復讀問題,都讀到state=0,select出了數據,都繼續往下執行了,導致money更新不對,那么要如何解決呢?
使用悲觀鎖,在sql中將第一句改為select * from order where orderId=1 and state=0 for update,在查的時候,十分悲觀,整個事務只允許一個連接進行操作,拿不到鎖的連接只能一直等著,等待拿到鎖的連接提交完事務釋放鎖資源。
悲觀鎖的缺點
因為只能保證一個連接進行操作,效率十分低。項目中查詢量比較大的情況下,不會使用悲觀鎖。
樂觀鎖
什么是樂觀鎖?
比較簡單,表示比較樂觀,它在別人在做修改的時候,不會上鎖,不過加稱為版本標識的判斷(類似CAS無鎖機制),主要特點是本質上沒有鎖,使用版本標識和影響行數(如version-版本號)進行控制,樂觀鎖有效的根本原因是sql執行的原子性。
回到具體例子,還是有兩個連接執行那三句sql:
select * from order where orderId=1 and state=0,如果select到內容繼續往下執行:
update set state=1 where orderId=1,
update money set money=moeny+money where orderId=1
使用樂觀鎖,Order表添加version字段,update的sql執行改為
update set state=1,set version=version+1 where orderId=1 and version=version
當一個連接執行select語句,獲取version版本號,update語句中where附加version進行查找,修改完畢后將version+1,此時另外一個連接也select到了數據,不過version是舊的,再根據舊的version就使用不了update語句了,此時它的影響行數為0。這里的影響行數就是數據庫返回給你的成功修改的行數值。
如果影響行數>0,執行第三句sql,第一個連接成功修改了第二句update語句,因此它的影響行數大于0,可以執行第三句sql。
悲觀鎖和樂觀鎖的區別?
使用場景的區別?
如果查詢量小,可以使用悲觀鎖,在請求量大時,多個請求來時,悲觀鎖只能讓一個請求執行。
使用樂觀鎖使用版本控制操作,要使用樂觀鎖的話需要自己在表中添加一個版本標識字段,常規下(絕大多數)使用樂觀鎖。
重入鎖
什么現象非重入鎖會產生而重入鎖不會產生?
死鎖現象
具體見之前的死鎖例子,當時已經有用到重入的概念了。
重入鎖有哪些?
在Java中,ReentrantLock和synchronized都是可重入鎖。
什么是重入鎖
重入鎖,又稱為遞歸鎖,指的是同一線程的外層函數獲得鎖資源之后,內層的遞歸函數仍然有該鎖使用權。
可重入鎖-synchronized例子
get中調用set方法,函數進行嵌套,鎖能夠進行傳遞
import lombok.SneakyThrows;class MyTThread implements Runnable {public void get() {System.out.println(Thread.currentThread().getId() + " get()");set();}@SneakyThrowspublic void set() {Thread.sleep(100);System.out.println(Thread.currentThread().getId() + " set()");}@Overridepublic void run() {get();} }public class Test2 {public static void main(String[] args) {MyTThread myTThread = new MyTThread();new Thread(myTThread).start();new Thread(myTThread).start();new Thread(myTThread).start();} }這段代碼執行結果是
11 get() 13 get() 12 get() 13 set() 11 set() 12 set()我們希望get(),set()交替執行,我們在兩個方法都加上synchronized關鍵字修飾即可
public synchronized void get() {System.out.println(Thread.currentThread().getId() + " get()");set();}@SneakyThrowspublic synchronized void set() {Thread.sleep(100);System.out.println(Thread.currentThread().getId() + " set()");}那么是為什么呢?
這就是因為synchronized是重入鎖,線程①進行get()方法后占用this鎖,調用set()時候,set方法仍然有this鎖的使用權,而其他的線程②和③因為獲取不到this鎖被阻塞,只能等待線程①的set()方法執行完畢釋放鎖。
執行結果如下:
11 get() 11 set() 13 get() 13 set() 12 get() 12 set()舉一反三,使用同步代碼塊的時候,只要外層和內層函數使用的是同一個鎖對象,那么就是可重入的;如果外層和內層函數使用的是不是同一個鎖對象,那么就是非可重入的。
可重入鎖-ReentrantLock例子
這個例子和上一個例子是一樣的,不作過多解釋
import lombok.SneakyThrows; import java.util.concurrent.locks.ReentrantLock;class MyTThread implements Runnable {private ReentrantLock lock = new ReentrantLock();public synchronized void get() {lock.lock();System.out.println(Thread.currentThread().getId() + " get()");set();lock.unlock();}@SneakyThrowspublic synchronized void set() {lock.lock();Thread.sleep(100);System.out.println(Thread.currentThread().getId() + " set()");lock.unlock();}@Overridepublic void run() {get();} }public class Test2 {public static void main(String[] args) {MyTThread myTThread = new MyTThread();new Thread(myTThread).start();new Thread(myTThread).start();new Thread(myTThread).start();} }重入鎖的好處?
外層函數能將鎖資源傳遞給內層函數,內層函數無需重新獲取鎖資源,效率提高。
注意的小點
內層函數釋放鎖資源不影響外層函數的鎖資源
讀寫鎖
考慮這樣一個場景,兩個線程對一個共享文件進行讀寫操作,同時讀是沒有問題的,但是如果有一個線程想去寫這個共享文件,那么就不應該有其他的線程對該資源進行讀或寫。
讀寫鎖的機制?
無論多少個線程如果都在讀,其他的線程可以讀或寫,只要一個線程正在寫,其他線程不可以讀或寫。
例子
首先是不加鎖的情況
import lombok.Data; import lombok.SneakyThrows;import java.util.HashMap; import java.util.Map;@Data class Cache {static private volatile Map<String, Object> map = new HashMap<>();@SneakyThrowspublic static Object write(String key, Object value){System.out.println("正在開始寫..." + ", key: " + key + ",value: " + value);Thread.sleep(100);Object o = map.put(key, value);System.out.println("結束寫..." + ", key: " + key + ",value: " + value);return o;}@SneakyThrowspublic static Object read(String key){System.out.println("正在開始讀..." + ", key: " + key);Thread.sleep(100);Object o = map.get(key);System.out.println("結束讀..." + ", key: " + key + ",value: " + o);return o;} }public class Test3 {public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {Cache.write(i + "", i + "");}}}).start();new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {Cache.read(i + "");}}}).start();} }運行結果可以看到,9正在寫,還沒有結束寫,9就開始讀了,讀的結果是null,數據發生了異常
如何解決呢?使用ReentrantReadwriteLock-讀寫鎖,使用起來還是十分容易的。
結果就不會出現寫一半的時候出現讀了
CAS無鎖機制
CAS是什么意思?
Compare And Swap
什么是CAS無鎖機制?
CAS無鎖機制和自旋鎖是配套使用的,因為自旋鎖的底層用的就是CAS無鎖機制,CAS無鎖機制效率非常高,CAS無鎖機制其實和樂觀鎖是類似的概念,本身沒有鎖,而是用一個標識。
CAS體系中有三個參數,分別是V,E,N,V表示要更新的值,E表示期望值,N表示新值,線程執行先判斷要更新的值V與期望值E,如果它們相同,說明沒有任何線程更改,線程繼續操作,將新值N覆蓋V;如果V和E不同,說明其他線程更改過,當前線程不做任何操作,只把N覆蓋V。
其實預期值E就是之前緩存的值,更新值V如果和預期值E不同的話,說明V被其他線程修改了,再進行操作共享數據將可能會發生沖突,所以不操作共享數據,只把新值N賦給V。
例子
看AtomicInteger的源碼,
public class Test1 {public static void main(String[] args) {new AtomicInteger().incrementAndGet();} }ctrl+左鍵點進去,我發現我這里的源碼和之前不一樣了,應該被重構過,改到更底層去實現了,但是再怎么底層原理應該是不變的。
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}ctrl+左鍵進入unsafe.getAndAddInt,值得注意的是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;}首先是var5 = this.getIntVolatile(var1, var2);表示通過對象和偏移量獲取變量的值,Unsafe類可以讓java操作內存地址,就是比如這里通過對象和偏移量直接從內存中獲取對應變量的值,取得的值賦給var5,這里的var5其實是要更新的值V,由于用了Volatile修飾,因此V是線程之間可見的,就是說不同的線程看到的V都是相同的一個值。
compareAndSwapInt方法是Java的native方法,并不由Java語言實現。
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);
方法的作用是,讀取傳入對象o在內存中偏移量為offset位置的值與期望值expected(上次緩存的V值)作比較。相等就把x(這里的x就是var5+var4,等同于V+1)值賦值給offset位置的值。方法返回true。不相等,就取消賦值,方法返回false,繼續執行getIntVolatile刷新V值,繼續和上次緩存了V值的E比較,直到在其它線程執行CAS操作之前,搶先退出循環操作,執行+1操作。
unsafe.getAndAddInt(this, valueOffset, 1) + 1;
自旋鎖
什么是自旋鎖
自旋鎖是采用讓當前線程不停的在循環體內執行實現的,當循環的條件被其他線程改變時才能進入臨界區。是不可重入的鎖
例子
下面例子展示自旋鎖現象,效果是線程卡死。
當第一個線程調用這個不可重入的自旋鎖去加鎖(調用lock函數)是沒有問題的,但當再次調用lock的時候,因為自旋鎖已經持有引用已經不為空了,該線程對象會誤認為是別人的線程持有自旋鎖,釋放不了鎖資源,程序直接卡死。
自旋鎖使用了CAS原子操作(compareAndSet方法),lock函數將所有者owner設置為當前線程,并且預測原來的值為空;unlock函數將所有者owner設置為空,并預測值為當前對象。
當有第二個線程調用lock方法時,由于owner值不為空(設置為了其他或者當前線程對象),導致循環一致被執行,直至第一個線程調用了unlock函數將owner設為空,第二個線程才能進入臨界區。
由于自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,所以響應速度更快。但當線程數不停增加時,性能下降明顯,因為每個線程都要執行,占用CPU時間
package ch6;import lombok.AllArgsConstructor; import lombok.Data;import java.sql.Statement; import java.util.concurrent.atomic.AtomicReference;class MySpinLock {// 原子類 作用是對對象的引用,它可以保證你在修改對象引用時的線程安全性private AtomicReference<Thread> sign = new AtomicReference<>();public void lock(){Thread current = Thread.currentThread();while (!sign.compareAndSet(null, current)){}}public void unlock(){Thread current = Thread.currentThread();sign.compareAndSet(current, null);} } @Data @AllArgsConstructor public class Test2 implements Runnable{static int sum;private MySpinLock lock;public static void main(String[] args) {MySpinLock lock = new MySpinLock();for (int i = 0; i < 10; i++) {Test2 test2 = new Test2(lock);Thread thread =new Thread(test2);thread.start();}}@Overridepublic void run() {this.lock.lock();this.lock.lock();sum++;this.lock.unlock();this.lock.unlock();} }分布式鎖
//todo 先挖坑,挖坑填不填就不知道了
總結
以上是生活随笔為你收集整理的闭关修炼(六)各种锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 椭圆检测_使用OpenCV
- 下一篇: 渗透测试的目标、思路