0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

多線程常見鎖策略+CAS介紹

Android編程精選 ? 來源:CSDN ? 2023-03-14 16:30 ? 次閱讀

一、樂觀鎖 & 悲觀鎖

1.1 樂觀鎖的定義

樂觀鎖,顧名思義,他比較樂觀,他認為一般情況下不會出現(xiàn)沖突,所以只會在更新數(shù)據(jù)的時候才會對沖突進行檢測。如果沒有發(fā)生沖突直接進行修改,如果發(fā)生了沖突則不進行任何修改,然后把結果返回給用戶,讓用戶自行處理。

1.1.1樂觀鎖的實現(xiàn)-CAS

樂觀鎖的實現(xiàn)并不是給數(shù)據(jù)加鎖 ,而是通過CAS(Compare And Swap)比較并替換,來實現(xiàn)樂觀鎖的效果。

CAS比較并替換的流程是這樣子的:CAS中包含了三個操作,單位:V(內存值)、A(預期的舊址)、B(新值),比較V值和A值是否相等,,如果相等的話則將V的值更換成B,否則就提示用戶修改失敗,從而實現(xiàn)了CAS機制。

這只是定義的流程,但是在實際執(zhí)行過程中,并不會當V值和A值不相等時,就立即把結果返回給用戶,而是將A(預期的舊值)改為內存中最新的值,然后再進行比較,直到V值也A值相等,修改內存中的值為B結束。

可能你還是覺得有些晦澀,那我們舉個栗子:

41ee6676-bf62-11ed-bfe3-dac502259ad0.png

看完這個圖相信你一定能理解了CAS的執(zhí)行流程了。

1.1.2 CAS的應用

CAS的底層實現(xiàn)是靠Unsafe類實現(xiàn)的,Unsafe是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng),需要通過本地(Native)方法來訪問,Unsafe相當于一個后門,基于該類可以直接操作特定的內存數(shù)據(jù)。Unsafe類存在sun.misc包中,其內部方法操作可以像C的指針一樣直接操作內存,因為Java中的CAS操作的執(zhí)行依賴于Unsafe類的方法。

注意Unsafe類的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統(tǒng)底層資源執(zhí)行相應的任務。因此不推薦使用Unsafe類,如果用不好會對底層資源造成影響。

為什么Atomic修飾的包裝類,能夠保證原子性,依靠的就是底層的unsafe類,我們來看看AtomicInteger的源碼:

41f6b146-bf62-11ed-bfe3-dac502259ad0.png

在getAndIncrement方法中還調用了unsafe的方法,因此這也就是為什么它能夠保證原子性的原因。

因此我們可以利用Atomic+包裝類實現(xiàn)線程安全的問題。

importjava.util.concurrent.atomic.AtomicInteger;

/**
*使用AtomicInteger保證線程安全問題
*/
publicclassAtomicIntegerDemo{
staticclassCounter{
privatestaticAtomicIntegernum=newAtomicInteger(0);
privateintMAX_COUNT=100000;
publicCounter(intMAX_COUNT){
this.MAX_COUNT=MAX_COUNT;
}
//++方法
publicvoidincrement(){
for(inti=0;i{
counter.increment();
});
Threadthread2=newThread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最終結果:"+counter.getNum());
}
}
4207dbe2-bf62-11ed-bfe3-dac502259ad0.png

1.1.3 CAS存在的問題

循環(huán)時間長,開銷大

只能保證一個共享變量的原子性操作(可以通過循環(huán)CAS的方式實現(xiàn))

存在ABA問題

1.1.4 ABA問題

什么時ABA問題呢?

比如說兩個線程t1和t2,t1的執(zhí)行時間為10s,t2的執(zhí)行時間為2s,剛開始都從主內存中獲取到A值,t2先開始執(zhí)行,他執(zhí)行的比較快,于是他將A的值先改為B,再改為A,這時t1執(zhí)行,判斷內存中的值為A,與自己預期的值一樣,以為這個值沒有修改過,于是將內存中的值修改為B,但是實際上中間可能已經經歷了許多:A->B->A。

所以ABA問題就是,在我們進行CAS中的比較時,預期的值與內存中的值一樣,并不能說明這個值沒有被改過,而是可能已經被修改了,但是又被改回了預期的值。

importjava.util.concurrent.atomic.AtomicInteger;

/**
*ABA問題演示
*/
publicclassABADemo1{
privatestaticAtomicIntegermoney=newAtomicInteger(100);
publicstaticvoidmain(String[]args)throwsInterruptedException{
//第一次點轉賬按鈕(-50)
Threadt1=newThread(()->{
intold_money=money.get();//先得到余額
try{//執(zhí)行花費2s
Thread.sleep(2000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
money.compareAndSet(old_money,old_money-50);
});
t1.start();

//第二次點擊轉賬按鈕(-50)不小心點擊的,因為第一次點擊之后沒反應,所以不小心又點了一次
Threadt2=newThread(()->{
intold_money=money.get();//先得到余額
money.compareAndSet(old_money,old_money-50);
});
t2.start();

//給賬戶加50
Threadt3=newThread(()->{
//執(zhí)行花費1s
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
intold_money=money.get();
money.compareAndSet(old_money,old_money+50);

});
t3.start();

t1.join();
t2.join();
t3.join();
System.out.println("最終的錢數(shù):"+money.get());
}
}
42189b62-bf62-11ed-bfe3-dac502259ad0.png

這個例子演示了ABA問題,A有100元,A向B轉錢,第一次轉了50元,但是點完轉賬按鈕沒有反應,于是又點擊了一次。第一次轉賬成功后A還剩50元,而這時C給A轉了50元,A的余額變?yōu)?00元,第二次的CAS判斷(100,100,50),A的余額與預期的值一樣,于是將A的余額修改為50元。

1.1.5 ABA問題的解決方案

由于CAS是只管頭和尾是否相等,若相等,就認為這個過程沒問題,因此我們就引出了AtomicStampedReference,時間戳原子引用,在這里應用于版本號的更新。也就是我們新增了一種機制,在每次更新的時候,需要比較當前值和期望值以及當前版本號和期望版本號,若值或版本號有一個不相同,這個過程都是有問題的。

我們來看上面的例子怎么用AtomicStampedReference解決呢?

importjava.util.concurrent.atomic.AtomicInteger;
importjava.util.concurrent.atomic.AtomicStampedReference;

/**
*ABA問題解決添加版本號
*/
publicclassABADemo2{
privatestaticAtomicStampedReferencemoney=
newAtomicStampedReference<>(100,0);
publicstaticvoidmain(String[]args)throwsInterruptedException{
//第一次點轉賬按鈕(-50)
Threadt1=newThread(()->{
intold_money=money.getReference();//先得到余額100
intoldStamp=money.getStamp();//得到舊的版本號
try{//執(zhí)行花費2s
Thread.sleep(2000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1);
System.out.println(Thread.currentThread().getName()+"轉賬:"+result);
},"線程1");
t1.start();

//第二次點擊轉賬按鈕(-50)不小心點擊的,因為第一次點擊之后沒反應,所以不小心又點了一次
Threadt2=newThread(()->{
intold_money=money.getReference();//先得到余額100
intoldStamp=money.getStamp();//得到舊的版本號
booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1);
System.out.println(Thread.currentThread().getName()+"轉賬:"+result);
},"線程2");
t2.start();

//給賬戶+50
Threadt3=newThread(()->{
//執(zhí)行花費1s
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
intold_money=money.getReference();//先得到余額100
intoldStamp=money.getStamp();//得到舊的版本號
booleanresult=money.compareAndSet(old_money,old_money+50,oldStamp,oldStamp+1);
System.out.println(Thread.currentThread().getName()+"發(fā)工資:"+result);

},"線程3");
t3.start();

t1.join();
t2.join();
t3.join();
System.out.println("最終的錢數(shù):"+money.getReference());
}
}
422cd712-bf62-11ed-bfe3-dac502259ad0.png

AtommicStampedReference解決了ABA問題,在每次更新值之前,比較值和版本號。

1.2 悲觀鎖

什么是悲觀鎖?

悲觀鎖就是比較悲觀,總是假設最壞的情況,每次去拿數(shù)據(jù)的時候都會認為別人會修改,所以在每次拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿數(shù)據(jù)就會阻塞直到它拿到鎖。

比如我們之前提到的synchronized和Lock都是悲觀鎖。

二、公平鎖和非公平鎖

公平鎖: 按照線程來的先后順序獲取鎖,當一個線程釋放鎖之后,那么就喚醒阻塞隊列中第一個線程獲取鎖。

4235bb02-bf62-11ed-bfe3-dac502259ad0.png

非公平鎖: 不是按照線程來的先后順序喚醒鎖,而是當有一個線程釋放鎖之后,喚醒阻塞隊列中的所有線程,隨機獲取鎖。

424aa724-bf62-11ed-bfe3-dac502259ad0.png

之前在講synchronized和Lock這兩個鎖解決線程安全問題線程安全問題的解決的時候,我們提過:

synchronized的鎖只能是非公平鎖;

Lock的鎖默認情況下是非公平鎖,而擋在構造 函數(shù)中傳入參數(shù)時,則是公平鎖;

公平鎖:Lock lock=new ReentrantLock(true);

非公平鎖:Lock lock=new ReentrantLock();

由于公平鎖只能按照線程來的線程順序獲取鎖,因此性能較低,推薦使用非公平鎖。

三、讀寫鎖

3.1 讀寫鎖

讀寫鎖顧名思義是一把鎖分為兩部分:讀鎖和寫鎖。

讀寫鎖的規(guī)則是:允許多個線程獲取讀鎖,而寫鎖是互斥鎖,不允許多個線程同時獲得,并且讀操作和寫操作也是 互斥的,總的來說就是讀讀不互斥,讀寫互斥,寫寫互斥。

為什么要這樣設置呢?

讓整個讀寫的操作到設置為互斥不是更方便嗎?

其實只要涉及到“互斥”,就會產生線程掛起等待,一旦掛起等待,,再次被喚醒就不知道什么時候了,因此盡可能的減少“互斥"的機會,就是提高效率的重要途徑。

Java標準庫提供了ReentrantReadWriteLock類實現(xiàn)了讀寫鎖。

ReentrantReadWriteLock.ReadLock類表示一個讀鎖,提供了lock和unlock進行加鎖和解鎖。

ReentrantReadWriteLock.WriteLock類表示一個寫鎖,提供了lock和unlock進行加鎖和解鎖。

下面我們來看下讀寫鎖的使用演示~

importjava.time.LocalDateTime;
importjava.util.concurrent.LinkedBlockingDeque;
importjava.util.concurrent.ThreadPoolExecutor;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.locks.ReentrantReadWriteLock;

/**
*演示讀寫鎖的使用
*/
publicclassReadWriteLockDemo1{
publicstaticvoidmain(String[]args){
//創(chuàng)建讀寫鎖
finalReentrantReadWriteLockreentrantReadWriteLock=newReentrantReadWriteLock();
//創(chuàng)建讀鎖
finalReentrantReadWriteLock.ReadLockreadLock=reentrantReadWriteLock.readLock();
//創(chuàng)建寫鎖
finalReentrantReadWriteLock.WriteLockwriteLock=reentrantReadWriteLock.writeLock();
//線程池
ThreadPoolExecutorexecutor=newThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,newLinkedBlockingDeque<>(100));
//啟動線程執(zhí)行任務【讀操作1】
executor.submit(()->{
//加鎖操作
readLock.lock();
try{
//執(zhí)行業(yè)務邏輯
System.out.println("執(zhí)行讀鎖1:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
readLock.unlock();
}
});

//啟動線程執(zhí)行任務【讀操作2】
executor.submit(()->{
//加鎖操作
readLock.lock();
try{
//執(zhí)行業(yè)務邏輯
System.out.println("執(zhí)行讀鎖2:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
//釋放鎖
readLock.unlock();
}
});

//啟動線程執(zhí)行【寫操作1】
executor.submit(()->{
//加鎖
writeLock.lock();
try{
System.out.println("執(zhí)行寫鎖1:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
writeLock.unlock();
}
});

//啟動線程執(zhí)行【寫操作2】
executor.submit(()->{
//加鎖
writeLock.lock();
try{
System.out.println("執(zhí)行寫鎖2:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
writeLock.unlock();
}
});
}
}
426afbbe-bf62-11ed-bfe3-dac502259ad0.png

根據(jù)運行結果我們看到,讀鎖操作是一起執(zhí)行的,而寫鎖操作是互斥執(zhí)行的。

3.2 獨占鎖

獨占鎖就是指任何時候只能有一個線程能執(zhí)行資源操作,是互斥的。

比如寫鎖,就是一個獨占鎖,任何時候只能有一個線程執(zhí)行寫操作,synchronized、Lock都是獨占鎖。

3.3 共享鎖

共享鎖是指可以同時被多個線程獲取,但是只能被一個線程修改。讀寫鎖就是一個典型的共享鎖,它允許多個線程進行讀操作 ,但是只允許一個線程進行寫操作。

四、可重入鎖 & 自旋鎖

4.1 可重入鎖

可重入鎖指的是該線程獲取了該鎖之后,可以無限次的進入該鎖。

因為在對象頭存儲了擁有當前鎖的id,進入鎖之前驗證對象頭的id是否與當前線程id一致,若一致就可進入,因此實現(xiàn)可重入鎖 。

4.2 自旋鎖

自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采取循環(huán)的方式嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗。線程上下文切換就是從用戶態(tài)—>內核態(tài)。

synchronized就是一種自適應自旋鎖(自旋的次數(shù)不固定),hotSpot虛擬機的自旋機制是這一次的自旋次數(shù)由上一次自旋獲取鎖的次數(shù)來決定,如果上次自旋了很多次才獲取到鎖,那么這次自旋的次數(shù)就會降低,因為虛擬機認為這一次大概率還是要自旋很多次才能獲取到鎖,比較浪費系統(tǒng)資源。




審核編輯:劉清

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 虛擬機
    +關注

    關注

    1

    文章

    888

    瀏覽量

    27812
  • CAS
    CAS
    +關注

    關注

    0

    文章

    34

    瀏覽量

    15160
  • ABAT
    +關注

    關注

    0

    文章

    2

    瀏覽量

    6266

原文標題:一篇文章搞定,多線程常見鎖策略+CAS

文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    Java多線程的用法

    本文將介紹一下Java多線程的用法。 基礎介紹 什么是多線程 指的是在一個進程中同時運行多個線程,每個
    的頭像 發(fā)表于 09-30 17:07 ?843次閱讀

    C++面向對象多線程編程 (pdf電子版)

    C++面向對象多線程編程共分13章,全面講解構建多線程架構與增量多線程編程技術。第1章介紹
    發(fā)表于 09-25 09:39 ?0次下載

    QNX環(huán)境下多線程編程

    介紹了QNX 實時操作系統(tǒng)和多線程編程技術,包括線程間同步的方法、多線程程序的分析步驟、線程基本程序結構以及實用編譯方法。QNX 是由加拿大
    發(fā)表于 08-12 17:37 ?30次下載

    多線程技術在串口通信中的應用

            首先介紹多線程技術的基本原理,然后討論了多線程技術在串口通信中的應用,并給出了實現(xiàn)的方法和步驟。關鍵詞:
    發(fā)表于 09-04 09:10 ?18次下載

    多線程細節(jié)問題學習筆記

    這一次我們要說下關于final在多線程的作用,原子性的使用,死鎖以及Java中的應對方案,線程的局部變量 和 讀寫介紹 。關于final變量在
    發(fā)表于 11-28 15:34 ?1081次閱讀
    <b class='flag-5'>多線程</b>細節(jié)問題學習筆記

    多線程好還是單線程好?單線程多線程的區(qū)別 優(yōu)缺點分析

    摘要:如今單線程多線程已經得到普遍運用,那么到底多線程好還是單線程好呢?單線程多線程的區(qū)別又
    發(fā)表于 12-08 09:33 ?8.1w次閱讀

    mfc多線程編程實例及代碼,mfc多線程間通信介紹

    摘要:本文主要以MFC多線程為中心,分別對MFC多線程的實例、MFC多線程之間的通信展開的一系列研究,下面我們來看看原文。
    發(fā)表于 12-08 15:23 ?1.7w次閱讀
    mfc<b class='flag-5'>多線程</b>編程實例及代碼,mfc<b class='flag-5'>多線程</b>間通信<b class='flag-5'>介紹</b>

    什么是多線程編程?多線程編程基礎知識

    摘要:多線程編程是現(xiàn)代軟件技術中很重要的一個環(huán)節(jié)。要弄懂多線程,這就要牽涉到多進程。本文主要以多線程編程以及多線程編程相關知識而做出的一些結論。
    發(fā)表于 12-08 16:30 ?1.2w次閱讀

    java學習——java面試【事務、、多線程】資料整理

    本文檔內容介紹了基于java學習java面試【事務、、多線程】資料整理,供參考
    發(fā)表于 03-13 13:53 ?0次下載

    labview AMC多線程

    labview_AMC多線程
    發(fā)表于 08-21 10:31 ?27次下載

    CAS如何實現(xiàn)各種無的數(shù)據(jù)結構

    ,可用于在多線程編程中實現(xiàn)不被打斷的數(shù)據(jù)交換操作,從而避免多線程同時改寫某?數(shù)據(jù)時由于執(zhí)行順序不確定性以及中斷的不可預知性產?的數(shù)據(jù)不一致問題 有了CAS,我們就可以用它來實現(xiàn)各種無
    的頭像 發(fā)表于 11-13 15:38 ?619次閱讀
    無<b class='flag-5'>鎖</b><b class='flag-5'>CAS</b>如何實現(xiàn)各種無<b class='flag-5'>鎖</b>的數(shù)據(jù)結構

    多線程同步的幾種方法

    多線程同步是指在多個線程并發(fā)執(zhí)行的情況下,為了保證線程執(zhí)行的正確性和一致性,需要采用特定的方法來協(xié)調線程之間的執(zhí)行順序和共享資源的訪問。下面將介紹
    的頭像 發(fā)表于 11-17 14:16 ?960次閱讀

    多線程如何保證數(shù)據(jù)的同步

    。本文將詳細介紹多線程數(shù)據(jù)同步的概念、問題、以及常見的解決方案。 一、多線程數(shù)據(jù)同步概念 在多線程編程中,數(shù)據(jù)同步指的是通過某種機制來確保多
    的頭像 發(fā)表于 11-17 14:22 ?891次閱讀

    mfc多線程編程實例

    (圖形用戶界面)應用程序的開發(fā)。在這篇文章中,我們將重點介紹MFC中的多線程編程。 多線程編程在軟件開發(fā)中非常重要,它可以實現(xiàn)程序的并發(fā)執(zhí)行,提高程序的效率和響應速度。MFC提供了豐富的多線程
    的頭像 發(fā)表于 12-01 14:29 ?1143次閱讀

    java實現(xiàn)多線程的幾種方式

    了多種實現(xiàn)多線程的方式,本文將詳細介紹以下幾種方式: 1.繼承Thread類 2.實現(xiàn)Runnable接口 3.Callable和Future 4.線程池 5.Java 8中
    的頭像 發(fā)表于 03-14 16:55 ?436次閱讀