程序猿必須要掌握的多線程安全問題之鎖策略詳解
一、常見的鎖策略
1.1 樂觀鎖
樂觀鎖:樂觀鎖假設(shè)認(rèn)為數(shù)據(jù)一般情況下不會(huì)產(chǎn)生并發(fā)沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時(shí)候,才會(huì)正 式對數(shù)據(jù)是否產(chǎn)生并發(fā)沖突進(jìn)行檢測,如果發(fā)現(xiàn)并發(fā)沖突了,則讓返回用戶錯(cuò)誤的信息,讓用戶決定如 何去做。樂觀鎖的性能比較高。
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì) 上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖。
悲觀鎖的問題:總是需要競爭鎖,進(jìn)而導(dǎo)致發(fā)生線程切換,掛起其他線程;所以性能不高。 樂觀鎖的問題:并不總是能處理所有問題,所以會(huì)引入一定的系統(tǒng)復(fù)雜度。
樂觀鎖的使用場景:
import java.util.concurrent.atomic.AtomicInteger; public class happylock { private static AtomicInteger count = new AtomicInteger(0); private static final int MAXSIZE = 100000; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { for(int i = 0;i<MAXSIZE;i++){ count.getAndIncrement(); } } }); t1.start(); t1.join(); Thread t2= new Thread(new Runnable() { @Override public void run() { for(int j = 0;j<MAXSIZE;j++){ count.getAndDecrement(); } } }); t2.start(); t2.join(); System.out.println("結(jié)果"+count); } //結(jié)果是0,如果不加AtomicInteger,那么線程執(zhí)行完以后不會(huì)是0,存在線程不安全! }
1.2 悲觀鎖
悲觀鎖:他認(rèn)為通常情況下會(huì)出現(xiàn)并發(fā)沖突,所以它在一開始就加鎖;
synchronized 就是悲觀鎖
1.3 讀寫鎖
多線程之間,數(shù)據(jù)的讀取方之間不會(huì)產(chǎn)生線程安全問題,但數(shù)據(jù)的寫入方互相之間以及和讀者之間都需 要進(jìn)行互斥。如果兩種場景下都用同一個(gè)鎖, 就會(huì)產(chǎn)生極大的性能損耗。所以讀寫鎖因此而產(chǎn)生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執(zhí)行加鎖操作時(shí)需要額外表明讀寫意圖,復(fù)數(shù)讀者之間并不互斥,而寫者則要求與任何人互斥。
把鎖分成兩個(gè)鎖,一個(gè)是讀鎖,一個(gè)是寫鎖,其中讀鎖可以多個(gè)線程擁有,而寫鎖是一個(gè)線程擁有。讀鎖是共享鎖,而寫鎖是非公享鎖。
讀寫鎖的應(yīng)用方法:
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Readerlock { //讀寫鎖的具體實(shí)現(xiàn) public static void main(String[] args) { //創(chuàng)建讀寫鎖 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //分離讀鎖 ReentrantReadWriteLock.ReadLock readLock= ReadWriteLock.ReadLock(); //分離寫鎖 ReentrantReadWriteLock.WriteLock readLock= ReadWriteLock.WriteLock(); } }
1.4 公平鎖與非公平鎖
公平鎖:鎖的獲取順序必須合線程方法的先后順序是保存一致的,就叫公平鎖 優(yōu)點(diǎn):執(zhí)行時(shí)順序的,所以結(jié)果是可以預(yù)期的
非公平鎖:鎖的獲取方式循序和線程獲取鎖的順序無關(guān)。優(yōu)點(diǎn):性能比較高
1.5 自旋鎖(Spin Lock)
按之間的方式處理下,線程在搶鎖失敗后進(jìn)入阻塞狀態(tài),放棄 CPU,需要過很久才能再次被調(diào)度。但經(jīng)過測算,實(shí)際的生活中,大部分情況下,雖然當(dāng)前搶鎖失敗,但過不了很久,鎖就會(huì)被釋放?;谶@個(gè) 事實(shí),自旋鎖誕生了。
你可以簡單的認(rèn)為自旋鎖就是下面的代碼
只要沒搶到鎖,就死等。
自旋鎖的缺點(diǎn):
缺點(diǎn)其實(shí)非常明顯,就是如果之前的假設(shè)(鎖很快會(huì)被釋放)沒有滿足,則線程其實(shí)是光在消耗 CPU 資源,長期在做無用功的。
1.6 可重入鎖
可重入鎖的字面意思是“可以重新進(jìn)入的鎖”,即允許同一個(gè)線程多次獲取同一把鎖。比如一個(gè)遞歸函數(shù) 里有加鎖操作,遞歸過程中這個(gè)鎖會(huì)阻塞自己嗎?如果不會(huì),那么這個(gè)鎖就是可重入鎖(因?yàn)檫@個(gè)原因 可重入鎖也叫做遞歸鎖)。
Java里只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現(xiàn)成的Lock實(shí)現(xiàn)類,包括
synchronized關(guān)鍵字鎖都是可重入的。
1.7 相關(guān)題目
面試題:
1.你是怎么理解樂觀鎖和悲觀鎖的,具體怎么實(shí)現(xiàn)呢?
樂觀鎖——> CAS ——> Atomic.(CAS是由v(內(nèi)存值) A(預(yù)期值)B(新值))組成,然后執(zhí)行的時(shí)候是使用V=A對比,如果結(jié)果為true,這表明沒有并發(fā)沖突,則可以直接進(jìn)行修改,否則返回錯(cuò)誤信息。*
2.有了解什么讀寫鎖么?
多線程之間,數(shù)據(jù)的讀取方之間不會(huì)產(chǎn)生線程安全問題,但數(shù)據(jù)的寫入方互相之間以及和讀者之間都需 要進(jìn)行互斥。如果兩種場景下都用同一個(gè)鎖,就會(huì)產(chǎn)生極大的性能損耗。所以讀寫鎖因此而產(chǎn)生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執(zhí)行加鎖操作時(shí)需要額外表明讀寫意圖,復(fù)數(shù)讀者之間并不互斥,而寫者則要求與任何人互斥。
把鎖分成兩個(gè)鎖,一個(gè)是讀鎖,一個(gè)是寫鎖,其中讀鎖可以多個(gè)線程擁有,而寫鎖是一個(gè)線程擁有
3.什么是自旋鎖,為什么要使用自旋鎖策略呢,缺點(diǎn)是什么?
按之間的方式處理下,線程在搶鎖失敗后進(jìn)入阻塞狀態(tài),放棄 CPU,需要過很久才能再次被調(diào)度。但經(jīng)過測算,實(shí)際的生活中,大部分情況下,雖然當(dāng)前搶鎖失敗,但過不了很久,鎖就會(huì)被釋放?;谶@個(gè) 事實(shí),自旋鎖誕生了。
你可以簡單的認(rèn)為自旋鎖就是下面的代碼
只要沒搶到鎖,就死等。
自旋鎖的缺點(diǎn):
缺點(diǎn)其實(shí)非常明顯,就是如果之前的假設(shè)(鎖很快會(huì)被釋放)沒有滿足,則線程其實(shí)是光在消耗 CPU 資源,長期在做無用功的。
4.synchronized 是可重入鎖么?
synchronized 是可重入鎖,
代碼如下:
public class Chonglock { private static Object lock = new Object(); public static void main(String[] args) { //第一次進(jìn)入鎖 synchronized (lock){ System.out.println("第一次進(jìn)入鎖"); synchronized (lock){ System.out.println("第二次進(jìn)入鎖"); } } } }
二、CAS問題
2.1 什么是CAS問題
CAS: 全稱Compare and swap,字面意思:”比較并交換“,一個(gè) CAS 涉及到以下操作:
我們假設(shè)內(nèi)存中的原數(shù)據(jù)V,舊的預(yù)期值A(chǔ),需要修改的新值B。 1. 比較 A 與 V 是否相等。(比較) 2. 如果比較相等,將 B 寫入 V。(交換) 3. 返回操作是否成功。
當(dāng)多個(gè)線程同時(shí)對某個(gè)資源進(jìn)行CAS操作,只能有一個(gè)線程操作成功,但是并不會(huì)阻塞其他線程,其他線程只會(huì)收到操作失敗的信號(hào)??梢?CAS 其實(shí)是一個(gè)樂觀鎖。
2.2 CAS 是怎么實(shí)現(xiàn)的
針對不同的操作系統(tǒng),JVM 用到了不同的 CAS 實(shí)現(xiàn)原理,簡單來講:
java 的 CAS 利用的的是 unsafe 這個(gè)類提供的 CAS 操作;
unsafe 的 CAS 依 賴 了 的 是 jvm 針 對 不 同 的 操 作 系 統(tǒng) 實(shí) 現(xiàn) 的 Atomic::cmpxchg(一個(gè)原子性的指令)
/Atomic::cmpxchg 的實(shí)現(xiàn)使用了匯編的 CAS 操作,并使用 cpu 硬件提供的 lock 機(jī)制保證其原子性。
簡而言之,是因?yàn)橛布枰粤酥С?,軟件層面才能做到?/p>
2.3 CAS 有哪些應(yīng)用
2.3.1 實(shí)現(xiàn)自旋鎖
public class SpinLock { private AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); // 不放棄 CPU,一直在這里旋轉(zhuǎn)判斷 while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign.compareAndSet(current, null); } }
用于實(shí)現(xiàn)原子類
示例代碼:
public class AtomicInteger { public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } } public class 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; } }
三、ABA問題
3.1 什么是ABA問題
ABA 的問題,就是一個(gè)值從A變成了B又變成了A,而這個(gè)期間我們不清楚這個(gè)過程。
3.2 實(shí)現(xiàn)ABA問題場景
我來舉一個(gè)例子,如果你向別人轉(zhuǎn)錢,你需要轉(zhuǎn)100元,但是你點(diǎn)擊了兩次轉(zhuǎn)錢,第一次會(huì)成功,但是第二次肯定會(huì)失敗,但是,在你點(diǎn)擊第二次轉(zhuǎn)錢的同一時(shí)刻,你的公司給你轉(zhuǎn)了100元工資,那么你就會(huì)莫名其妙的把100又轉(zhuǎn)了出去,你丟失了100,別人也沒有獲得100.
代碼演示:
1.正常轉(zhuǎn)錢流程
import java.util.concurrent.atomic.AtomicReference; public class Aba { //ABA問題的演示 private static AtomicReference money = new AtomicReference(100);//轉(zhuǎn)賬 public static void main(String[] args) { //轉(zhuǎn)賬線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("點(diǎn)擊第一次轉(zhuǎn)出100"+result); } }); t1.start(); //轉(zhuǎn)賬線程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("點(diǎn)擊第二次轉(zhuǎn)出100"+result); if(!result){ System.out.println("余額不足,無法轉(zhuǎn)賬!"); } } }); t2.start(); } }
2.錯(cuò)誤操作后:
import java.util.concurrent.atomic.AtomicReference; public class ABas { private static AtomicReference money = new AtomicReference(100);//轉(zhuǎn)賬 public static void main(String[] args) throws InterruptedException { //轉(zhuǎn)賬出線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("第一次"+result); } }); t1.start(); t1.join(); //轉(zhuǎn)入100 Thread t3 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(0,100); System.out.println("轉(zhuǎn)賬"+result); } }); t3.start(); //轉(zhuǎn)賬線程2 t3.join(); Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("第二次"+result); } }); t2.start(); } }
解決ABA方法
解決方法:加入版本信息,例如攜帶 AtomicStampedReference 之類的時(shí)間戳作為版本信息,保證不會(huì) 出現(xiàn)老的值。
代碼實(shí)現(xiàn):
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class Abaack { //private static AtomicReference money = new AtomicReference(100);//轉(zhuǎn)賬 private static AtomicStampedReference money = new AtomicStampedReference(100,1); // public static void main(String[] args) throws InterruptedException { //轉(zhuǎn)賬出線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0,1,2); System.out.println("第一次轉(zhuǎn)賬100:"+result); } }); t1.start(); t1.join(); //轉(zhuǎn)入100 Thread t3 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(0,100,2,3); System.out.println("其他人給你轉(zhuǎn)賬了100:"+result); } }); t3.start(); //轉(zhuǎn)賬線程2 t3.join(); Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0,1,2); System.out.println("第二次點(diǎn)擊轉(zhuǎn)賬100:"+result); } }); t2.start(); //Integer的高速緩存是-128--127(AtomicStampedReference) //如果大于127,那么就開始new對象了 /* * 解決方法,調(diào)整邊界值*/ } }
四、總結(jié)
以上就是今天要講的內(nèi)容,本文僅僅簡單介紹了鎖策略,解決線程安全。
到此這篇關(guān)于程序猿必須要掌握的多線程安全問題之鎖策略詳解的文章就介紹到這了,更多相關(guān)java鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java的PriorityBlockingQueue優(yōu)先級(jí)阻塞隊(duì)列代碼實(shí)例
這篇文章主要介紹了Java的PriorityBlockingQueue優(yōu)先級(jí)阻塞隊(duì)列代碼實(shí)例,PriorityBlockingQueue顧名思義是帶有優(yōu)先級(jí)的阻塞隊(duì)列,為了實(shí)現(xiàn)按優(yōu)先級(jí)彈出數(shù)據(jù),存入其中的對象必須實(shí)現(xiàn)comparable接口自定義排序方法,需要的朋友可以參考下2023-12-12java編程實(shí)現(xiàn)獲取服務(wù)器IP地址及MAC地址的方法
這篇文章主要介紹了java編程實(shí)現(xiàn)獲取機(jī)器IP地址及MAC地址的方法,實(shí)例分析了Java分別針對單網(wǎng)卡及多網(wǎng)卡的情況下獲取服務(wù)器IP地址與MAC地址的相關(guān)技巧,需要的朋友可以參考下2015-11-11spring boot 若依系統(tǒng)整合Ueditor部署時(shí)上傳圖片錯(cuò)誤問題
這篇文章主要介紹了spring boot 若依系統(tǒng)整合Ueditor部署時(shí)上傳圖片錯(cuò)誤問題,本文給大家分享問題解決方法,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10Java實(shí)例項(xiàng)目零錢通的實(shí)現(xiàn)流程
本篇文章為你帶來Java的一個(gè)新手實(shí)戰(zhàn)項(xiàng)目,是一個(gè)零錢通系統(tǒng),項(xiàng)目來自于B站韓順平老師,非常適合新手入門練習(xí),感興趣的朋友快來看看吧2022-03-03ActiveMQ基于zookeeper的主從(levelDB Master/Slave)搭建
這篇文章主要介紹了ActiveMQ基于zookeeper的主從levelDB Master/Slave搭建,以及Spring-boot下的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Apache Commons fileUpload文件上傳多個(gè)示例分享
這篇文章主要為大家分享了Apache Commons fileUpload文件上傳4個(gè)示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10關(guān)于Lambda表達(dá)式的方法引用和構(gòu)造器引用簡的單示例
這篇文章主要介紹了關(guān)于Lambda表達(dá)式的方法引用和構(gòu)造器引用簡的單示例,方法引用與構(gòu)造器引用可以使?Lambda?表達(dá)式的代碼塊更加簡潔<BR>,需要的朋友可以參考下2023-04-04Spring?Boot中的過濾器攔截器監(jiān)聽器使用技巧匯總
本文將介紹在Spring?Boot應(yīng)用程序中使用過濾器、攔截器和監(jiān)聽器的使用技巧,我們將討論它們之間的區(qū)別,以及何時(shí)使用它們,我們還將提供代碼示例,以幫助您在自己的應(yīng)用程序中使用它們2023-12-12關(guān)于Mybatis-Plus字段策略與數(shù)據(jù)庫自動(dòng)更新時(shí)間的一些問題
這篇文章主要介紹了關(guān)于Mybatis-Plus字段策略與數(shù)據(jù)庫自動(dòng)更新時(shí)間的一些問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10詳解Spring Security中權(quán)限注解的使用
這篇文章主要為大家詳細(xì)介紹一下Spring Security中權(quán)限注解的使用方法,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)或工作有一定參考價(jià)值,需要的可以參考一下2022-05-05