程序猿必須要掌握的多線程安全問題之鎖策略詳解
一、常見的鎖策略
1.1 樂觀鎖
樂觀鎖:樂觀鎖假設認為數據一般情況下不會產生並發沖突,所以在數據進行提交更新的時候,才會正 式對數據是否產生並發沖突進行檢測,如果發現並發沖突瞭,則讓返回用戶錯誤的信息,讓用戶決定如 何去做。樂觀鎖的性能比較高。
悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會 上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
悲觀鎖的問題:總是需要競爭鎖,進而導致發生線程切換,掛起其他線程;所以性能不高。 樂觀鎖的問題:並不總是能處理所有問題,所以會引入一定的系統復雜度。
樂觀鎖的使用場景:
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("結果"+count); } //結果是0,如果不加AtomicInteger,那麼線程執行完以後不會是0,存在線程不安全! }
1.2 悲觀鎖
悲觀鎖:他認為通常情況下會出現並發沖突,所以它在一開始就加鎖;
synchronized 就是悲觀鎖
1.3 讀寫鎖
多線程之間,數據的讀取方之間不會產生線程安全問題,但數據的寫入方互相之間以及和讀者之間都需 要進行互斥。如果兩種場景下都用同一個鎖, 就會產生極大的性能損耗。所以讀寫鎖因此而產生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執行加鎖操作時需要額外表明讀寫意圖,復數讀者之間並不互斥,而寫者則要求與任何人互斥。
把鎖分成兩個鎖,一個是讀鎖,一個是寫鎖,其中讀鎖可以多個線程擁有,而寫鎖是一個線程擁有。讀鎖是共享鎖,而寫鎖是非公享鎖。
讀寫鎖的應用方法:
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Readerlock { //讀寫鎖的具體實現 public static void main(String[] args) { //創建讀寫鎖 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); //分離讀鎖 ReentrantReadWriteLock.ReadLock readLock= ReadWriteLock.ReadLock(); //分離寫鎖 ReentrantReadWriteLock.WriteLock readLock= ReadWriteLock.WriteLock(); } }
1.4 公平鎖與非公平鎖
公平鎖:鎖的獲取順序必須合線程方法的先後順序是保存一致的,就叫公平鎖 優點:執行時順序的,所以結果是可以預期的
非公平鎖:鎖的獲取方式循序和線程獲取鎖的順序無關。優點:性能比較高
1.5 自旋鎖(Spin Lock)
按之間的方式處理下,線程在搶鎖失敗後進入阻塞狀態,放棄 CPU,需要過很久才能再次被調度。但經過測算,實際的生活中,大部分情況下,雖然當前搶鎖失敗,但過不瞭很久,鎖就會被釋放。基於這個 事實,自旋鎖誕生瞭。
你可以簡單的認為自旋鎖就是下面的代碼
隻要沒搶到鎖,就死等。
自旋鎖的缺點:
缺點其實非常明顯,就是如果之前的假設(鎖很快會被釋放)沒有滿足,則線程其實是光在消耗 CPU 資源,長期在做無用功的。
1.6 可重入鎖
可重入鎖的字面意思是“可以重新進入的鎖”,即允許同一個線程多次獲取同一把鎖。比如一個遞歸函數 裡有加鎖操作,遞歸過程中這個鎖會阻塞自己嗎?如果不會,那麼這個鎖就是可重入鎖(因為這個原因 可重入鎖也叫做遞歸鎖)。
Java裡隻要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現成的Lock實現類,包括
synchronized關鍵字鎖都是可重入的。
1.7 相關題目
面試題:
1.你是怎麼理解樂觀鎖和悲觀鎖的,具體怎麼實現呢?
樂觀鎖——> CAS ——> Atomic.(CAS是由v(內存值) A(預期值)B(新值))組成,然後執行的時候是使用V=A對比,如果結果為true,這表明沒有並發沖突,則可以直接進行修改,否則返回錯誤信息。*
2.有瞭解什麼讀寫鎖麼?
多線程之間,數據的讀取方之間不會產生線程安全問題,但數據的寫入方互相之間以及和讀者之間都需 要進行互斥。如果兩種場景下都用同一個鎖,就會產生極大的性能損耗。所以讀寫鎖因此而產生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執行加鎖操作時需要額外表明讀寫意圖,復數讀者之間並不互斥,而寫者則要求與任何人互斥。
把鎖分成兩個鎖,一個是讀鎖,一個是寫鎖,其中讀鎖可以多個線程擁有,而寫鎖是一個線程擁有
3.什麼是自旋鎖,為什麼要使用自旋鎖策略呢,缺點是什麼?
按之間的方式處理下,線程在搶鎖失敗後進入阻塞狀態,放棄 CPU,需要過很久才能再次被調度。但經過測算,實際的生活中,大部分情況下,雖然當前搶鎖失敗,但過不瞭很久,鎖就會被釋放。基於這個 事實,自旋鎖誕生瞭。
你可以簡單的認為自旋鎖就是下面的代碼
隻要沒搶到鎖,就死等。
自旋鎖的缺點:
缺點其實非常明顯,就是如果之前的假設(鎖很快會被釋放)沒有滿足,則線程其實是光在消耗 CPU 資源,長期在做無用功的。
4.synchronized 是可重入鎖麼?
synchronized 是可重入鎖,
代碼如下:
public class Chonglock { private static Object lock = new Object(); public static void main(String[] args) { //第一次進入鎖 synchronized (lock){ System.out.println("第一次進入鎖"); synchronized (lock){ System.out.println("第二次進入鎖"); } } } }
二、CAS問題
2.1 什麼是CAS問題
CAS: 全稱Compare and swap,字面意思:”比較並交換“,一個 CAS 涉及到以下操作:
我們假設內存中的原數據V,舊的預期值A,需要修改的新值B。 1. 比較 A 與 V 是否相等。(比較) 2. 如果比較相等,將 B 寫入 V。(交換) 3. 返回操作是否成功。
當多個線程同時對某個資源進行CAS操作,隻能有一個線程操作成功,但是並不會阻塞其他線程,其他線程隻會收到操作失敗的信號。可見 CAS 其實是一個樂觀鎖。
2.2 CAS 是怎麼實現的
針對不同的操作系統,JVM 用到瞭不同的 CAS 實現原理,簡單來講:
java 的 CAS 利用的的是 unsafe 這個類提供的 CAS 操作;
unsafe 的 CAS 依 賴 瞭 的 是 jvm 針 對 不 同 的 操 作 系 統 實 現 的 Atomic::cmpxchg(一個原子性的指令)
/Atomic::cmpxchg 的實現使用瞭匯編的 CAS 操作,並使用 cpu 硬件提供的 lock 機制保證其原子性。
簡而言之,是因為硬件予以瞭支持,軟件層面才能做到。
2.3 CAS 有哪些應用
2.3.1 實現自旋鎖
public class SpinLock { private AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); // 不放棄 CPU,一直在這裡旋轉判斷 while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign.compareAndSet(current, null); } }
用於實現原子類
示例代碼:
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 的問題,就是一個值從A變成瞭B又變成瞭A,而這個期間我們不清楚這個過程。
3.2 實現ABA問題場景
我來舉一個例子,如果你向別人轉錢,你需要轉100元,但是你點擊瞭兩次轉錢,第一次會成功,但是第二次肯定會失敗,但是,在你點擊第二次轉錢的同一時刻,你的公司給你轉瞭100元工資,那麼你就會莫名其妙的把100又轉瞭出去,你丟失瞭100,別人也沒有獲得100.
代碼演示:
1.正常轉錢流程
import java.util.concurrent.atomic.AtomicReference; public class Aba { //ABA問題的演示 private static AtomicReference money = new AtomicReference(100);//轉賬 public static void main(String[] args) { //轉賬線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("點擊第一次轉出100"+result); } }); t1.start(); //轉賬線程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0); System.out.println("點擊第二次轉出100"+result); if(!result){ System.out.println("餘額不足,無法轉賬!"); } } }); t2.start(); } }
2.錯誤操作後:
import java.util.concurrent.atomic.AtomicReference; public class ABas { private static AtomicReference money = new AtomicReference(100);//轉賬 public static void main(String[] args) throws InterruptedException { //轉賬出線程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(); //轉入100 Thread t3 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(0,100); System.out.println("轉賬"+result); } }); t3.start(); //轉賬線程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 之類的時間戳作為版本信息,保證不會 出現老的值。
代碼實現:
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class Abaack { //private static AtomicReference money = new AtomicReference(100);//轉賬 private static AtomicStampedReference money = new AtomicStampedReference(100,1); // public static void main(String[] args) throws InterruptedException { //轉賬出線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0,1,2); System.out.println("第一次轉賬100:"+result); } }); t1.start(); t1.join(); //轉入100 Thread t3 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(0,100,2,3); System.out.println("其他人給你轉賬瞭100:"+result); } }); t3.start(); //轉賬線程2 t3.join(); Thread t2 = new Thread(new Runnable() { @Override public void run() { boolean result = money.compareAndSet(100,0,1,2); System.out.println("第二次點擊轉賬100:"+result); } }); t2.start(); //Integer的高速緩存是-128--127(AtomicStampedReference) //如果大於127,那麼就開始new對象瞭 /* * 解決方法,調整邊界值*/ } }
四、總結
以上就是今天要講的內容,本文僅僅簡單介紹瞭鎖策略,解決線程安全。
到此這篇關於程序猿必須要掌握的多線程安全問題之鎖策略詳解的文章就介紹到這瞭,更多相關java鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!