synchronized的輕量級鎖居然不會自旋?

《Java并發編程的藝術》中說到「如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖」,并且下文所配的流程圖中明確表示自旋失敗后才會升級為重量級鎖,但《深入理解Java虛擬機》又說「如果出現兩條以上的線程爭用同一個鎖的情況,那輕量級鎖就不再有效,必須要膨脹為重量級鎖」,到底會不會呢?其實相信synchronized源碼很少有人愿意去扒去看,本文會盡量用簡潔易懂的方式說清synchronized的原理。

只對實現原理感興趣可以直接跳過到「synchronized實現原理」

synchronized基本使用
一般有三種方式:

修飾普通方法:鎖this

  // 1. synchronized用在普通方法上,默認的鎖就是this,當前實例public synchronized void method() {}

修飾靜態方法:鎖this.class

  // 2. synchronized用在靜態方法上,默認的鎖就是當前所在的Class類// 所以無論是哪個線程訪問它,需要的鎖都只有一把public static synchronized void method() {}

同步代碼塊:自定義鎖對象
自定義鎖對象可以是實例,也可以是Class對象

synchronized (this) {}
synchronized(SynchronizedObjectLock.class){}

拋出異常會釋放鎖
無論正常退出還是拋出異常,synchronized都保證能夠釋放鎖。

鎖與happens-before規則
我們知道,解鎖操作 happens-before 加鎖,因此:

首先有個變量a,沒有用volatile修飾

int a = 0;

線程A先執行:

public synchronized void writer() { // 1a++; // 2
} // 3

線程B后執行:

public synchronized void reader() { // 4int i = a; // 5
} // 6 

由h-b規則,3 h-b 4,再由as if serial和傳遞性原則,因此2 h-b 5,而h-b從開發人員的角度來說,你就可以理解為2在5之前執行,并且2的結果對5可見,因此5處讀到的a,一定為1。

synchronized的內存語義
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。

當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效

可以看到:

鎖釋放與volatile寫有相同的內存語義;

鎖獲取與volatile讀有相同的內存語義。

synchronized實現原理
下面我用盡量清晰簡潔,繞過虛擬機源碼的方式來講一下:

會跳過一些源碼細節的實現,不會影響整體流程和理解

要了解實現原理,第一步我會先看一下字節碼指令:

透過字節碼看異常如何釋放鎖
synchronized修飾的方法會被加上 ACC_SYNCHRONIZED的flag。

而同步代碼塊的字節碼是這樣的:

monitorenter    
...
monitorexit 
goto xxx
monitorexit
athrow
returnException table:
from   to  target type4     14    17    any17    20    17    any

可以看到,monitorenter和monitorexit指令分別對應synchronized同步塊的進入和退出。

有兩個monitorexit,因為javac為同步代碼塊添加了一個隱式的try-finally,在finally中會調用monitorexit命令釋放鎖。如果不知道字節碼的Exception table是什么可以參考:異常處理實現原理

盡管字節碼通常都能幫助我們更好地理解語義,但關于synchronized的語義也就到此為止了,接下來就要深入虛擬機源碼看看monitorenter(獲取鎖)和monitorexit(釋放鎖)到底都干了些什么,不過在此之前:

因為synchronized有四種鎖狀態,而鎖狀態的實現依賴于Java對象的mark word,這是實現synchronized的基礎,我們先來看mark word如何表達鎖狀態的。

Java中的每一個對象都可以作為一個鎖,包括Class對象。

四種鎖狀態
Java對象頭的mark word
在這里插入圖片描述

注意輕/重鎖的mark word內是持有一個指向鎖記錄的指針的。

因此,一個對象其實有四種鎖狀態,級別由低到高:

無鎖狀態

偏向鎖狀態

輕量級鎖狀態

重量級鎖狀態

1、無鎖
釋放輕量級鎖,沒有線程在嘗試獲取鎖,也沒有線程持有鎖(正在執行同步代碼塊),就是無鎖。

2、偏向鎖(JDK15被廢棄)
偏向鎖在JDK1.6引入,在JDK15被廢棄,了解即可。如果一定要用,需要手動打開:

-XX:+UseBiasedLocking

人們發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,于是有了偏向鎖。

偏向鎖顧名思義,偏向于第一個訪問鎖的線程。偏向鎖在資源無競爭情況下消除了同步語句,連CAS操作都不做了,提高了程序的運行性能。

當開啟偏向鎖功能時,創建的新對象是可偏向狀態,此時mark word中的thread id為0,也叫做匿名偏向。當該對象第一次被CAS成功時,成為「偏向鎖」。

在該線程又一次嘗試獲取該對象鎖時,發現thread id就是自己,就可以不做CAS直接認為已經拿到了鎖并執行同步代碼塊中的代碼。

注意上述的所有,都只出現了一個線程

當第二個線程出現并嘗試獲取鎖,無論如何都會升級成「輕量級鎖」。

如果第一個線程正在執行同步代碼塊,鎖偏向的線程繼續擁有鎖,當前線程升級該鎖為「輕量級鎖」。

如果第一個線程不在執行同步代碼塊,先將對象頭的mark word改為無鎖狀態,再升級為「輕量級鎖」。

也就是是要有兩個線程嘗試獲取鎖,不論是否出現資源競爭,升級為「輕量級鎖」。

3、輕量級鎖
升級到「輕量級鎖」的條件是:存在多個線程嘗試CAS獲取同一把鎖,盡管彼此之間互不影響。而「輕量級鎖」繼續膨脹為「重量級鎖」的條件是:只要CAS失敗,就升級,即發生了:一個線程正在執行同步代碼塊的同時,另一個線程嘗試獲取鎖。

輕量級鎖會自旋嗎
自旋:不斷嘗試去獲取鎖,一般用循環來實現。

這是不對的,是網上最常見的錯誤之一,你問chatGPT他也是這個答案,但這就是個錯誤的答案。因為前面說的很清楚了,只要發生哪怕一次CAS失敗,就不是「輕量級鎖」了,何來自旋呢?

自旋的說法從何而來
《Java并發編程的藝術》(2015)原文是:

線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

《深入淺出Java多線程1.0.0》原文是:

然后線程嘗試用CAS將鎖的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示Mark Word已經被替換成了其他線程的鎖記錄,說明在與其它線程競爭鎖,當前線程就嘗試使用自旋來獲取鎖。

總之,以上兩位作者認為:發生競爭,自旋,并沒有指出自旋前會發生鎖膨脹。

《深入理解Java虛擬機》(2019)原文是:

如果這個更新操作失敗了,那就意味著至少存在一條線程與當前線程競爭獲取該對象的鎖。虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是,說明當前線程已經擁有了這個對象的鎖,那直接進入同步塊繼續執行就可以了,否則就說明這個鎖對象已經被其他線程搶占了。如果出現兩條以上的線程爭用同一個鎖的情況,那輕量級鎖就不再有效,必須要膨脹為重量級鎖,鎖標志的狀態值變為“10”,此時Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也必須進入阻塞狀態。

周志明大大的意思是:出現兩條以上的線程爭用同一個鎖的情況,就要升級為重量級鎖,沒有指出升級為重量級鎖前要自旋。

顯然,這兩種觀點是有沖突的,核心問題在于:

輕量級鎖狀態下,發生資源競爭,到底是自旋,還是立刻鎖膨脹?

如何考證說法的正確性
那么我們也只能自己去看源碼來驗證說法的正確性了(但很少有人愿意看吧)

下文我會盡量清楚地用文字表達出源碼傳達的意思

輕量級鎖實現原理
獲取鎖
發現是無鎖狀態,線程會把鎖的Mark Word復制到自己的Displaced Mark Word(棧幀中的一塊空間) ,然后通過CAS嘗試將鎖的Mark Word修改為一根指針,指向自己的Displaced Mark Word(Displaced Mark Word與原mark word的內容一模一樣,保存了HashCode,GC年齡等信息)

發現處于輕量級鎖狀態

如果輕量級鎖的markword指向自己的Displaced Mark Word,代表重入鎖,那么獲取鎖成功(如果是重入,會將markword改為null,空指針,即0)

如果輕量級鎖的markword不是指向自己,鎖膨脹,升級為「重量級鎖」

CAS失敗直接膨脹

釋放鎖
首先,遍歷線程棧,拿到所有需要做解鎖操作的鎖對象:

如果是null,代表可重入的鎖,直接解鎖成功

如果不是重入的鎖:

還原成功,輕量級鎖解鎖成功

還原失敗,仍然是「嘗試解鎖重量級鎖」

如果markword被修改,說明發生了競爭,已經成為「重量級鎖」了,「嘗試解鎖重量級鎖」

如果markword沒被修改,嘗試CAS還原對象的markword

補充說明:線程A正在執行同步代碼塊時,此時有線程CAS失敗,雖然升級為「重量級鎖」,但仍然由線程A持有鎖,「如何膨脹為重量級鎖」后文馬上分析

4、重量級鎖
為了實現鎖膨脹,避免并發膨脹鎖,定義了四種膨脹鎖狀態:

膨脹完畢

膨脹中

無鎖

輕量級鎖

下面依次對這些情況的膨脹進行分析:

重量級鎖的生成/鎖膨脹
若膨脹完畢,直接返回monitor

若膨脹中,線程等待一會,直到別的線程膨脹完畢,然后拿到別人生成的monitor

從輕量級鎖開始膨脹:

創建monitor對象

CAS將鎖狀態修改為「膨脹中」

將markword保存至monitor

設置持有monitor的線程

將monitor地址設置為mark word

返回monitor對象

失敗,說明別人在膨脹了,等待,然后返回別人生成的monitor

成功:

從無鎖開始膨脹,差不多:

創建monitor對象

將markword保存至monitor

CAS將鎖狀態修改為「膨脹中」

失敗,說明別人在膨脹了,等待,然后返回別人生成的monitor

成功,返回monitor對象

重量級鎖實現原理
生成了重量級鎖,mark word會指向堆中實際生成的monitor對象,我們先來看看monitor對象的結構:
在這里插入圖片描述
Contention List(cxq):所有請求鎖的線程將被首先放置到該競爭隊列,是先進后出的棧結構

Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List

Wait Set:那些調用wait方法被阻塞的線程被放置到Wait Set

OnDeck:任何時刻最多只能有一個線程正在競爭鎖,該線程稱為OnDeck

Owner:獲得鎖的線程稱為Owner

!Owner:釋放鎖的線程

獲取鎖
對于重量級鎖,嘗試獲取鎖具體是指:嘗試用CAS將monitor對象的Owner從nullptr改變為自己

當一個線程嘗試獲得重量級鎖時

首先嘗試「自旋」,調用trySpin方法獲取鎖,如果第一次失敗,再進行一次trySpin方法(最壞情況拿不到鎖會調用兩次trySpin),然后『用CAS的方式進入cxq』

進入cxq后,陷入「死循環」,死循環中,可能會從cxq轉移到EntryList,可能阻塞,也可能調用trySpin方法自旋。后文再詳細分析「死循環」

可以看到「死循環」的實現也依賴trySpin自旋,因此我們先來看看「自旋」的實現邏輯:

1、自旋鎖
自旋:不斷嘗試去獲取鎖,一般用循環來實現。

如果是單核CPU,自旋是無意義的,所以只有多處理器才會開啟自旋功能

自旋的出現,是為了避免切換到內核態,因為線程的阻塞和喚醒依賴內核,我們希望能夠一定程度上避免這種內核態與用戶態的切換,因此有了「自旋鎖」。那么自旋多少次更合適呢?

在鎖很快被釋放時,自旋既不會帶來CPU資源的浪費,還能提高運行效率。此時自旋次數過少,可能會導致沒能順利拿到鎖,即使結束自旋后不久鎖就被釋放了。

在鎖很久才被釋放時,自旋空轉占用CPU資源卻遲遲拿不到鎖,造成過多的CPU資源浪費。此時自旋次數過多,反而會得不償失。

因此,JDK發明了自適應自旋,來適應各種情況的鎖。

自適應自旋
自適應自旋為了權衡自旋次數過多和過少帶來的弊端,它的基本思想是:

自旋成功拿到鎖了,說明你下次成功的概率也很大,下次自旋的次數會更多

自旋失敗,說明你下次也大概率拿不到,下次自旋的次數會更少

自適應自旋參數如下:
在這里插入圖片描述
自旋邏輯:trySpin
首選預自旋11次(避免預自旋次數設置為0,源碼后面對這個參數加了1),如果沒拿到鎖:

開始自旋5000次(假設是第一次開始自旋,上限就為5000)

成功,下次+100,下次可以最多自旋5100次

失敗,下次- 200,下次可以最多自旋4800次,不會少于1000次

2、死循環
死循環主要是在「阻塞」和「自旋」之間切換

park阻塞,注意不會移動到WaitSet中

unpark喚醒,再次調用trySpin方法自旋獲取鎖,如果失敗,陷入阻塞

只有釋放鎖時,才會調用unpark喚醒,進入自旋狀態,此時并不是一定能拿到鎖的。

喚醒的時機
釋放鎖時才會喚醒,且只會喚醒一個,喚醒邏輯取決于Policy參數。

cxq和EntryList內線程的行為
這兩個區域內的線程幾乎是全阻塞的,這兩個區域內的線程,保證最多只有一個線程去競爭鎖資源,這個被『釋放鎖時喚醒的唯一的線程』叫「假定繼承人」,即Monitor結構中的「OnDeck」。

注意:只保證所有阻塞的線程,只有一個去競爭鎖資源,仍然可能被外來的線程在進入cxq之前就搶到了鎖,所以說synchronized是不公平的。

EntryList內的線程全部來自cxq,在釋放鎖與調用notify方法時,可能進入EntryList

釋放鎖
通過CAS的方式將Monitor結構的Owner修改為nullptr

根據QMode參數的不同,執行不同的邏輯

因為QMode默認值為0,我們來看一下默認的邏輯:

如果EntryList和cxq均為空:什么也不做

如果EntryList非空:就取EntryList首元素喚醒

如果EntryList為空,cxq非空:將cxq的所有線程放到EntryList,再喚醒EntryList首元素;

鎖被持有時,EntryList和cxq的所有線程都阻塞,有且只有鎖釋放這唯一一個行為能夠喚醒其中的一個線程。

為什么要區分cxq和EntryList
是為了解決CAS的ABA問題,也能分散請求,提高性能。

cxq和EntryList都是為了存儲所有阻塞的線程,但是:

釋放鎖并喚醒時,只會喚醒EntryList的線程,這是刪除操作

線程自旋次數過多需要被阻塞時,只會插入cxq隊列,這是添加操作

把這兩種操作分離開來有什么好處呢?

提高性能
由于鎖只有一把,因此做刪除操作的線程只有一個,不存在線程安全問題,不需要做CAS,如果和添加操作混在一起,就不得不考慮線程安全問題了。這樣只需要在cxq內考慮CAS即可。

解決ABA問題
因為多個線程同時add,不會有某個線程出現在cxq里兩次,因此只add不會有ABA問題。而一旦存在刪除操作,那么ABA問題就是有可能的。

可感知的鎖控制權
現在知道了加解鎖的原理,那其實我們已經有能力知道,釋放鎖時會喚醒哪個線程。(暫時不考慮wait/notify)

結論:先阻塞的線程,最晚獲得鎖。

有三個線程,t1,t2,t3。這三個線程都自旋失敗,插入cxq,由于是個棧,越晚進入cxq的,反而越早進入EntryList,順序為t3,t2,t1。而喚醒時是按照EntryList的順序去喚醒的,因此「并不是所謂的隨機喚醒」。當然,如果此時有別的線程t4自旋未進入cxq,是有可能拿到鎖的,但我們保證:t3先于t2被喚醒,t2先于t1被喚醒

階段性小結(一)
到這里,應該對鎖機制非常熟悉了,你應該清楚:

Monitor鎖結構

自旋的原理和應用,自旋不會出現在輕量級鎖

重量級鎖加解鎖的邏輯

我們趁熱打鐵來學習一下wait/notify的底層原理,至今仍未露面的WaitSet終于要登場了,學完wait/notify整個synchronized也就 “證據鏈閉環” 了。

從趁熱打鐵的角度,趁你還對加解鎖和Monitor結構足夠熟悉,我非常推薦直接跳到「wait/notify底層原理」看,當然,在此之前請確保你對wait/notify的基礎知識足夠了解

等待通知機制:wait/notify
wait/notify必備的基礎知識
wait/notify只能用在synchronized代碼塊內部,且必須是重量級鎖。

只有持有鎖的線程能夠調用wait/notify方法

調用wait會使當前線程釋放鎖并陷入阻塞狀態

從wait()方法返回的前提是獲得了調用對象的鎖

可以喚醒一個(notify)或多個(notifyAll)

調用notify無法保證被喚醒的線程一定拿到鎖

當調用一個鎖對象的wait或notify方法時,如當前鎖的狀態是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。

wait/notify基本使用
等待通知基本模型
等待者:


synchronized(對象) {while(條件不滿足) {對象.wait();}對應的處理邏輯
}

通知者:

synchronized(對象) {改變條件對象.notifyAll();
}

等待超時模型
這樣一個熟悉的場景:調用一個方法時等待一段時間(一般來說是給定一個時間段),如果該方法能夠在給定的時間段之內得到結果,那么將結果立刻返回,反之,超時返回默認結果。

public synchronized Object get(long mills) throws InterruptedException {long future = System.currentTimeMillis() + mills;long remaining = mills;// 當超時大于0并且result返回值不滿足要求while ((result == null) && remaining > 0) {wait(remaining);remaining = future - System.currentTimeMillis();}return result;
}

wait/notify底層原理
wait方法
將當前線程包裝成ObjectWaiter對象,放入WaitSet中,并調用park掛起

執行「釋放鎖」的邏輯。

只有notify方法有可能將線程從WaitSet拯救出來,處于WaitSet的線程永遠是阻塞狀態,不可能參與鎖競爭

notify方法
從WaitSet中取出第一個線程,根據Policy的不同,將這個線程放入EntryList或者cxq隊列中的起始或末尾位置

默認Policy為2,即:

EntryList隊列為空,將線程放入EntryList

EntryList隊列非空,將線程放入cxq隊列的頭部位置(棧頂);

強調一下:notify方法只是將線程從WaitSet移動到EntryList或者cxq,不是直接讓它開始自旋CAS。

wait/notify理解實戰
看下面這段代碼,在不修改 HotSpot VM源碼的情況下,考慮幾個問題:

輸出唯一確定嗎?

如果確定,會輸出什么?

public class NotifyDemo {
private static void log(String desc){System.out.println(Thread.currentThread().getName() + " : " + desc);
}Object lock = new Object();public void startThreadA(){new Thread(() -> {synchronized (lock){log("get lock");startThreadB();log("start wait");try {lock.wait();}catch(InterruptedException e){e.printStackTrace();}log("get lock after wait");log("release lock");}}, "thread-A").start();
}public void startThreadB(){new Thread(()->{synchronized (lock){log("get lock");startThreadC();sleep(100);log("start notify");lock.notify();log("release lock");}},"thread-B").start();
}public void startThreadC(){new Thread(() -> {synchronized (lock){log("get lock");log("release lock");}}, "thread-C").start();
}public static void main(String[] args){new NotifyDemo().startThreadA();}
}

輸出唯一確定,為:

thread-A : get lock
thread-A : start wait
thread-B : get lock
thread-B : start notify
thread-B : release lock
thread-A : get lock after wait
thread-A : release lock
thread-C : get lock
thread-C : release lock

為什么最后四行A一定先于C發生?

線程C獲取鎖失敗,直接放入cxq首部;線程A被notify,會被放入EntryList。之后B釋放鎖,發現EntryList內有線程A,就直接把A喚醒。

自定義搶鎖邏輯:修改JVM參數
有兩個參數會影響synchronized的行為邏輯:

Policy參數:喚醒線程
Policy參數決定如何喚醒線程

Policy == 0:放入EntryList隊列的排頭位置;

Policy == 1:放入EntryList隊列的末尾位置;

Policy == 2:EntryList隊列為空就放入EntryList,否則放入cxq隊列的排頭位置;

Policy == 3:放入cxq隊列中,末尾位置

QMode參數:釋放鎖
QMode參數決定如何釋放鎖

QMode = 2,并且cxq非空:取cxq隊列排頭位置的ObjectWaiter對象,喚醒該線程,結束

QMode = 3,把cxq隊列的全部元素放入EntryList尾部,然后執行步驟四;

QMode = 4,把cxq隊列的全部元素放入EntryList頭部,然后執行步驟四;

QMode = 0,不做什么,執行步驟4;(默認為0)

如果EntryList非空,就取首元素喚醒,否則整個cxq放到EntryList,再喚醒EntryList首元素;

通過修改這兩個參數,就可以自定義notify和釋放鎖的邏輯。還是上面那個例子,只需要修改QMode為4,就可以確保最后四行C先于A執行。

階段性小結(二)
其實wait/notify原理并不難懂,甚至可以說是非常好理解,就不再重復了。

到此為止,與synchronized的原理基本就講解完畢了,接下來我們重新審視一下一些比較籠統而泛泛的問題,不僅能幫助你更好地理解synchronized的原理,也能對synchronized有一個更全面的認知。算是一些補充說明吧。

synchronized的特點
非公平鎖
非公平鎖完全可以從前文的原理體現出來:

新來的線程不斷自旋不會阻塞,因此比起阻塞中的線程,更容易搶占鎖

cxq先入后出,先陷入阻塞的線程反而更晚執行

notify喚醒的線程,如果EntrySet為空直接放入EntrySet,先于cxq被執行

可重入性
synchronized是可重入的

monitor有個計數器recursions,起初為0,Monitorenter + 1,Monitorexit - 1,減為0會釋放鎖。

樂觀 or 悲觀
什么是悲觀鎖,什么是樂觀鎖?

看似簡單的概念,很多人第一次學習時都會顧名思義,但現在網絡上主流的觀點有兩種:

樂觀鎖只是一種思想,認為不會競爭鎖,僅此而已

樂觀鎖是線程先執行鎖區域的內容,執行過程中檢查是否出現競爭

核心的矛盾點在于,樂觀鎖到底是純思想,還是對實現做了一些行為規范的定義(比如必須:什么都不操作直接執行同步代碼塊的內容)?

如果讀者有關于「樂觀鎖」較為官方的定義,請在評論區告訴我,感激不盡

但如果「樂觀鎖」僅僅是一種思想,那可以說:synchronized的所有線程,只要沒有被阻塞,那就是樂觀的,只有重量級鎖中那些在cxq和EntryList的阻塞的線程是悲觀的(WaitSet是自愿阻塞不算在內)。因為如果足夠悲觀,早就阻塞等待去了,為啥還要自旋CAS呢?

編譯器對synchronized的優化
鎖消除
如果編譯器發現不會發生線程安全問題,就會無視了你的鎖。

鎖粗化
比如執行插入數據商品時,是對店鋪加鎖。那么批量執行的時候,只需要加一次鎖。而不是每插入一次就加/釋放一次鎖。

  StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);// 線程安全的buffer類,append會加鎖,但顯然這是可以鎖粗話的,會優化成只獲得/釋放一次鎖synchronized與包裝類的坑
Integer并不適合當作鎖對象。

因為有緩存機制,-128~127有緩存。容易導致鎖失效。

volatile static Integer ticket = 10

比如兩個線程搶票,不能鎖住 ticket。搶完票以后ticket–,一個線程A鎖的是ticket = 10的對象,另一個線程B執行完ticket = 10的臨界區代碼,ticket–,再走臨界區,他的鎖變成了9,與A競爭的都不是一把鎖,因此兩者都會搶到鎖。

因此:

鎖住的對象盡量是靜態的不變的,比如class類

不能是各種有緩存的包裝類

在idea中 沒有聲明final的對象加synchronized會提示不安全

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/165992.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/165992.shtml
英文地址,請注明出處:http://en.pswp.cn/news/165992.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

超聲波雪深傳感器冬季里的科技魔法

在冬季的某個清晨,當你打開大門,被厚厚的積雪覆蓋的大地映入眼簾,你是否曾想過,這片雪地的深度是多少?它又如何影響著我們的生活和環境?今天,我們將為你揭開這個謎團,介紹一款神秘的…

眼鏡清洗機原理是怎么樣的?2023年眼鏡清洗機推薦

在日常生活中有許多小伙伴是因為看太多書或者是看太多電子產品導致近視佩戴上了眼鏡,畢竟眼鏡佩戴上后就再也離不開它了,像日常佩戴的眼鏡上會積累非常多污垢以及堆積細菌,而我們手動清洗眼鏡時不能除菌也不能清潔到縫隙中,像眼鏡…

thingsboard的WebSocket API的使用

1、參考文檔 Working with telemetry data | ThingsBoard Community Edition 2、訂閱的命令 我們需要訂閱不同的數據,那么該如何來填寫參數呢,你需要參考后端代碼 TelemetryPluginCmdsWrapper 以及訂閱返回的結果參考類:TelemetrySubscriptionUpdate 鏈接地址: https:/…

error: ‘for‘ loop initial declarations are only allowed in C99 or C11 mode

在使用for循環時,在循環內定義變量,出現如下錯誤 [Error] ‘for’ loop initial declarations are only allowed in C99 or C11 mode [Note] use option -stdc99,-stdgnu99,-stdc11 or-stdgnu11 to compile your code 出現這個錯誤…

使用Pytorch從零開始構建GRU

門控循環單元 (GRU) 是 LSTM 的更新版本。讓我們揭開這個網絡的面紗并探索這兩個兄弟姐妹之間的差異。 您聽說過 GRU 嗎?門控循環單元(GRU)是更流行的長短期記憶(LSTM)網絡的弟弟,也是循環神經網絡&#x…

極智AI | LLM大模型部署框架之OpenLLM

歡迎關注我的公眾號 [極智視界],獲取我的更多經驗分享 大家好,我是極智視界,本文來介紹一下 LLM大模型部署框架之OpenLLM。 邀您加入我的知識星球「極智視界」,星球內有超多好玩的項目實戰源碼下載,鏈接:https://t.zsxq.com/0aiNxERDq 由于 LLM 大模型在模型結構、模型規…

圖像評價之計算PSNR、SSIM、MSE、LPIPS和NIQE評價指標

文章目錄 鏈接說明代碼峰值信噪比結構相似性均方誤差學習感知圖像塊相似性自然圖像質量評估器 鏈接 GitHub 倉庫 如果代碼對你的研究有所幫助,請為該倉庫點上小星星。 說明 PSNR、SSIM、MSE和LPIPS是有監督指標,計算時必須是兩對圖像參與;…

安索夫矩陣(ANSOFF)

👉安索夫矩陣是策略管理之父安索夫博士于1957年提出的營銷策略模型,該模型以“產品”和“市場”作為兩大基本面,提出了4種不同組合下的營銷策略,是營銷分析中應用最廣泛的工具之一。其主要邏輯是通過選擇4種不同的成長性策略來實現…

C++11的線程

線程的創建 用std::thread創建線程非常簡單&#xff0c;只需要提供線程函數或者線程對象即可&#xff0c;并可以同時指定線程函數的參數。下面是創建線程的示例&#xff1a; #include <thread> #include <iostream> using namespace std;void func() {cout <<…

C# 獲取圖像、字體等對象大小的數據結構SizeF

如果你想要獲取字符串 "你好嗎" 的字節數組長度或者字符數&#xff0c; 使用如下代碼&#xff1a; string s "你好嗎"; //字節數組長度 int byteCount System.Text.Encoding.UTF8.GetBytes(s).Length; //字符數 int charCount s.Length; 如果你想獲取…

大話設計模式C++實現

大話設計模式&#xff0c;講得非常好&#xff0c;但是作者是用C#寫的&#xff0c;為了方便C程序員&#xff0c;使用C寫了大話設計模式的代碼 詳情見Github&#xff1a;https://github.com/liubamboo/BigTalkDesignPattern

新蘋果手機如何導入舊手機數據?解決方案來了,記得收藏!

為了保持其競爭優勢&#xff0c;蘋果公司不斷推出新的產品和服務&#xff0c;因此蘋果手機的更新換代速度是比較快的。正巧最近剛出了iPhone15&#xff0c;相信很多小伙伴已經換上了期待已久的新手機。 更換新手機后&#xff0c;大家都會面臨一個問題&#xff1a;新蘋果手機如…

java 手機商城免費搭建+電商源碼+小程序+三級分銷+SAAS云平臺

【SAAS云平臺】打造全行業全渠道全場景的SaaS產品&#xff0c;為店鋪經營場景提供一體化解決方案&#xff1b;門店經營區域化、網店經營一體化&#xff0c;本地化、全方位、一站式服務&#xff0c;為多門店提供統一運營解決方案&#xff1b;提供豐富多樣的營銷玩法覆蓋所有經營…

Windows DOS 常用命令

文章目錄 1 概述1.1 官方文檔1.2 常用 2 分類2.1 目錄2.2 文件2.3 網絡2.4 系統 1 概述 1.1 官方文檔 Windows 命令官方文檔&#xff1a;https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/windows-commands 1.2 常用 win r # 打開運…

如何預防數據泄露?六步策略幫您打造企業信息安全壁壘

大家好&#xff01;我是恒小馳&#xff0c;今天我想和大家聊聊一個非常重要的話題——如何預防數據泄露。在這個數字化的時代&#xff0c;數據已經成為了我們生活中不可或缺的一部分。然而&#xff0c;隨著數據的價值日益凸顯&#xff0c;數據泄露的風險也隨之增加。企業應該如…

MacBook使用指南

一、安裝及卸載Windows系統 1、卸載Windows系統 步驟① 點擊下側任務欄中的“啟動臺”&#xff0c;進入程序塢&#xff0c;點擊"其他",選擇“啟動轉換助理” 步驟② 點擊“繼續”&#xff0c;接著點擊“恢復”&#xff0c;即可卸載Windows系統 2、安裝Windows系統 …

Shell編程里if的參數從-a到-z詳解

Shell編程里if的參數從-a到-z詳解

智能醫療越發周到!新的機器人系統評估中風后的活動能力

原創 | 文 BFT機器人 中風是在醫療界上最難的解決的病例之一&#xff0c;全球每年有超過1500萬人中風&#xff0c;四分之三的中風患者的手臂和手部會出現損傷、虛弱和癱瘓。 許多中風患者日常生活是依靠他們強壯的手臂來完成的&#xff0c;從拿一些小東西到梳頭&#xff0c;即…

phpstudy和IDEA 配置php debug

1.安裝xdebug 擴展&#xff0c;phpinfo() 查看 2.配置php.ini zend_extensionD:/phpstudy_pro/Extensions/php/php7.4.3nts/ext/php_xdebug.dll xdebug.collect_params1 xdebug.collect_return1 xdebug.auto_traceOn xdebug.trace_output_dirD:/phpstudy_pro/Extensions/php_l…