程序猿必須要掌握的多線程安全問題之鎖策略詳解
一、常見的鎖策略
1.1 樂觀鎖
樂觀鎖:樂觀鎖假設(shè)認為數(shù)據(jù)一般情況下不會產(chǎn)生并發(fā)沖突,所以在數(shù)據(jù)進行提交更新的時候,才會正 式對數(shù)據(jù)是否產(chǎn)生并發(fā)沖突進行檢測,如果發(fā)現(xiàn)并發(fā)沖突了,則讓返回用戶錯誤的信息,讓用戶決定如 何去做。樂觀鎖的性能比較高。
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時候都認為別人會修改,所以每次在拿數(shù)據(jù)的時候都會 上鎖,這樣別人想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖。
悲觀鎖的問題:總是需要競爭鎖,進而導致發(fā)生線程切換,掛起其他線程;所以性能不高。 樂觀鎖的問題:并不總是能處理所有問題,所以會引入一定的系統(tǒng)復雜度。
樂觀鎖的使用場景:
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í)行完以后不會是0,存在線程不安全!
}
1.2 悲觀鎖
悲觀鎖:他認為通常情況下會出現(xiàn)并發(fā)沖突,所以它在一開始就加鎖;
synchronized 就是悲觀鎖
1.3 讀寫鎖
多線程之間,數(shù)據(jù)的讀取方之間不會產(chǎn)生線程安全問題,但數(shù)據(jù)的寫入方互相之間以及和讀者之間都需 要進行互斥。如果兩種場景下都用同一個鎖, 就會產(chǎn)生極大的性能損耗。所以讀寫鎖因此而產(chǎn)生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執(zhí)行加鎖操作時需要額外表明讀寫意圖,復數(shù)讀者之間并不互斥,而寫者則要求與任何人互斥。
把鎖分成兩個鎖,一個是讀鎖,一個是寫鎖,其中讀鎖可以多個線程擁有,而寫鎖是一個線程擁有。讀鎖是共享鎖,而寫鎖是非公享鎖。
讀寫鎖的應用方法:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Readerlock {
//讀寫鎖的具體實現(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)點:執(zhí)行時順序的,所以結(jié)果是可以預期的
非公平鎖:鎖的獲取方式循序和線程獲取鎖的順序無關(guān)。優(yōu)點:性能比較高
1.5 自旋鎖(Spin Lock)
按之間的方式處理下,線程在搶鎖失敗后進入阻塞狀態(tài),放棄 CPU,需要過很久才能再次被調(diào)度。但經(jīng)過測算,實際的生活中,大部分情況下,雖然當前搶鎖失敗,但過不了很久,鎖就會被釋放?;谶@個 事實,自旋鎖誕生了。
你可以簡單的認為自旋鎖就是下面的代碼
只要沒搶到鎖,就死等。
自旋鎖的缺點:
缺點其實非常明顯,就是如果之前的假設(shè)(鎖很快會被釋放)沒有滿足,則線程其實是光在消耗 CPU 資源,長期在做無用功的。
1.6 可重入鎖
可重入鎖的字面意思是“可以重新進入的鎖”,即允許同一個線程多次獲取同一把鎖。比如一個遞歸函數(shù) 里有加鎖操作,遞歸過程中這個鎖會阻塞自己嗎?如果不會,那么這個鎖就是可重入鎖(因為這個原因 可重入鎖也叫做遞歸鎖)。
Java里只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現(xiàn)成的Lock實現(xiàn)類,包括
synchronized關(guān)鍵字鎖都是可重入的。
1.7 相關(guān)題目
面試題:
1.你是怎么理解樂觀鎖和悲觀鎖的,具體怎么實現(xiàn)呢?
樂觀鎖——> CAS ——> Atomic.(CAS是由v(內(nèi)存值) A(預期值)B(新值))組成,然后執(zhí)行的時候是使用V=A對比,如果結(jié)果為true,這表明沒有并發(fā)沖突,則可以直接進行修改,否則返回錯誤信息。*
2.有了解什么讀寫鎖么?
多線程之間,數(shù)據(jù)的讀取方之間不會產(chǎn)生線程安全問題,但數(shù)據(jù)的寫入方互相之間以及和讀者之間都需 要進行互斥。如果兩種場景下都用同一個鎖,就會產(chǎn)生極大的性能損耗。所以讀寫鎖因此而產(chǎn)生。
讀寫鎖(readers-writer lock),看英文可以顧名思義,在執(zhí)行加鎖操作時需要額外表明讀寫意圖,復數(shù)讀者之間并不互斥,而寫者則要求與任何人互斥。
把鎖分成兩個鎖,一個是讀鎖,一個是寫鎖,其中讀鎖可以多個線程擁有,而寫鎖是一個線程擁有
3.什么是自旋鎖,為什么要使用自旋鎖策略呢,缺點是什么?
按之間的方式處理下,線程在搶鎖失敗后進入阻塞狀態(tài),放棄 CPU,需要過很久才能再次被調(diào)度。但經(jīng)過測算,實際的生活中,大部分情況下,雖然當前搶鎖失敗,但過不了很久,鎖就會被釋放?;谶@個 事實,自旋鎖誕生了。
你可以簡單的認為自旋鎖就是下面的代碼
只要沒搶到鎖,就死等。
自旋鎖的缺點:
缺點其實非常明顯,就是如果之前的假設(shè)(鎖很快會被釋放)沒有滿足,則線程其實是光在消耗 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 涉及到以下操作:
我們假設(shè)內(nèi)存中的原數(shù)據(jù)V,舊的預期值A(chǔ),需要修改的新值B。 1. 比較 A 與 V 是否相等。(比較) 2. 如果比較相等,將 B 寫入 V。(交換) 3. 返回操作是否成功。
當多個線程同時對某個資源進行CAS操作,只能有一個線程操作成功,但是并不會阻塞其他線程,其他線程只會收到操作失敗的信號??梢?CAS 其實是一個樂觀鎖。
2.2 CAS 是怎么實現(xiàn)的
針對不同的操作系統(tǒng),JVM 用到了不同的 CAS 實現(xiàn)原理,簡單來講:
java 的 CAS 利用的的是 unsafe 這個類提供的 CAS 操作;
unsafe 的 CAS 依 賴 了 的 是 jvm 針 對 不 同 的 操 作 系 統(tǒng) 實 現(xiàn) 的 Atomic::cmpxchg(一個原子性的指令)
/Atomic::cmpxchg 的實現(xiàn)使用了匯編的 CAS 操作,并使用 cpu 硬件提供的 lock 機制保證其原子性。
簡而言之,是因為硬件予以了支持,軟件層面才能做到。

2.3 CAS 有哪些應用
2.3.1 實現(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);
}
}
用于實現(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 的問題,就是一個值從A變成了B又變成了A,而這個期間我們不清楚這個過程。
3.2 實現(xiàn)ABA問題場景
我來舉一個例子,如果你向別人轉(zhuǎn)錢,你需要轉(zhuǎn)100元,但是你點擊了兩次轉(zhuǎn)錢,第一次會成功,但是第二次肯定會失敗,但是,在你點擊第二次轉(zhuǎn)錢的同一時刻,你的公司給你轉(zhuǎn)了100元工資,那么你就會莫名其妙的把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("點擊第一次轉(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("點擊第二次轉(zhuǎn)出100"+result);
if(!result){
System.out.println("余額不足,無法轉(zhuǎn)賬!");
}
}
});
t2.start();
}
}

2.錯誤操作后:
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 之類的時間戳作為版本信息,保證不會 出現(xiàn)老的值。
代碼實現(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("第二次點擊轉(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)文章
@RequestParam?和@RequestBody注解的區(qū)別解析
在 Spring MVC 中,我們可以使用 @RequestParam 和 @RequestBody 來獲取請求參數(shù),但它們在用法和作用上有一些區(qū)別,這篇文章主要介紹了@RequestParam?和@RequestBody注解的區(qū)別,需要的朋友可以參考下2023-06-06
maven為MANIFEST.MF文件添加內(nèi)容的方法
這篇文章主要介紹了maven為MANIFEST.MF文件添加內(nèi)容的方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
Spring?boot?security權(quán)限管理集成cas單點登錄功能的實現(xiàn)
這篇文章主要介紹了Spring?boot?security權(quán)限管理集成cas單點登錄,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
form-data與x-www-form-urlencoded的區(qū)別以及知識延伸
這篇文章主要給大家介紹了關(guān)于form-data與x-www-form-urlencoded的區(qū)別以及知識延伸,form-data和x-www-form-urlencoded都是HTTP請求中用于傳輸表單數(shù)據(jù)的編碼格式,需要的朋友可以參考下2023-11-11
詳談jvm--Java中init和clinit的區(qū)別
下面小編就為大家?guī)硪黄斦刯vm--Java中init和clinit的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10

