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