主頁 > 後端開發 > 并發編程

并發編程

2021-12-23 06:12:51 後端開發

Synchronized 相關問題

問題一:Synchronized 用過嗎,其原理是什么?

這是一道 Java 面試中幾乎百分百會問到的問題,因為沒有任何寫過并 發程式的開發者會沒聽說或者沒接觸過 Synchronized,
Synchronized 是由 JVM 實作的一種實作互斥同步的一種方式,如果你查看被 Synchronized 修飾過的程式塊編譯后的位元組碼,會發現, 被 Synchronized 修飾過的程式塊,在編譯前后被編譯器生成了monitorenter 和 monitorexit 兩個位元組碼指令,

這兩個指令是什么意思呢?
在虛擬機執行到 monitorenter 指令時,
首先要嘗試獲取物件的鎖: 如果這個物件沒有鎖定,或者當前執行緒已經擁有了這個物件的鎖,把鎖的計數器 +1;
當執行 monitorexit 指令時將鎖計數器 -1;
當計數器為 0 時,鎖就被釋放了,
如果獲取物件失敗了,那當前執行緒就要阻塞等待,直到物件鎖被另外一個執行緒釋放為止,
Java 中 Synchronize 通過在物件頭設定標記,達到了獲取鎖和釋放鎖的目的,

問題二:你剛才提到獲取物件的鎖,這個 "鎖" 到底是什么? 如何確定 物件的鎖?

"鎖" 的本質其實是 monitorenter 和 monitorexit 位元組碼指令的一個 Reference 型別的引數,即要鎖定和解鎖的物件,
Synchronized 可以修飾不同的物件,因此,對應的物件鎖可以這么確 定,
1. 如果 Synchronized 明確指定了鎖物件,比如 Synchronized(變數 名)、Synchronized(this) 等,說明加解鎖物件為該物件,
2. 如果沒有明確指定:
若 Synchronized 修飾的方法為非靜態方法,表示此方法對應的物件為鎖物件;
若 Synchronized 修飾的方法為靜態方法,則表示此方法對應的類物件為鎖物件,
注意,當一個物件被鎖住時,物件里面所有用 Synchronized 修飾的方法都將產生堵塞,而物件里非 Synchronized 修飾的方法可正常被呼叫,不受鎖影響,

問題三:什么是可重入性,為什么說 Synchronized 是可重入鎖?

可重入性是鎖的一個基本要求,是為了解決自己鎖死自己的情況,
一個類中的同步方法呼叫另一個同步方法,假如 Synchronized 不支持重入,進入 method2 方法時當前執行緒獲得鎖,method2 方法里面執行 method1 時當前執行緒又要去嘗試獲取鎖,這時如果不支持重入,它就要等釋放,把自己阻塞,導致自己鎖死自己,
對 Synchronized 來說,可重入性是顯而易見的,剛才提到,在執行 monitorenter 指令時,如果這個物件沒有鎖定,或者當前執行緒已經擁有了這個物件的鎖(而不是已擁有了鎖則不能繼續獲取),就把鎖的計數器 +1,其實本質上就通過這種方式實作了可重入性,

問題四:JVM 對 Java 的原生鎖做了哪些優化?

在 Java 6 之前,Monitor 的實作完全依賴底層作業系統的互斥鎖來實作,也就是我們剛才在問題二中所闡述的獲取/釋放鎖的邏輯,
由于 Java 層面的執行緒與作業系統的原生執行緒有映射關系,如果要將一 個執行緒進行阻塞或喚起都需要作業系統的協助,這就需要從用戶態切換 到內核態來執行,這種切換代價十分昂貴,很耗處理器時間,現代 JDK 中做了大量的優化,

一種優化是使用自旋鎖,即在把執行緒進行阻塞操作之前先讓執行緒自旋等 待一段時間,可能在等待期間其他執行緒已經解鎖,這時就無需再讓執行緒 執行阻塞操作,避免了用戶態到內核態的切換,

現代 JDK 中還提供了三種不同的 Monitor 實作,也就是三種不同的鎖:

偏向鎖(Biased Locking)
輕量級鎖
重量級鎖

這三種鎖使得 JDK 得以優化 Synchronized 的運行,當 JVM 檢測 到不同的競爭狀況時,會自動切換到適合的鎖實作,這就是鎖的升級、 降級,

當沒有競爭出現時,默認會使用偏向鎖,

JVM 會利用 CAS 操作,在物件頭上的 Mark Word 部分設定執行緒 ID,以表示這個物件偏向于當前執行緒,所以并不涉及真正的互斥鎖,因為在很多應用場景中,大部分物件生命周期中最多會被一個執行緒鎖定, 使用偏斜鎖可以降低無競爭開銷,

如果有另一執行緒試圖鎖定某個被偏斜過的物件,JVM 就撤銷偏斜鎖, 切換到輕量級鎖實作,

輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖; 否則,進一步升級為重量級鎖,

問題五:為什么說 Synchronized 是非公平鎖?

非公平主要表現在獲取鎖的行為上,并非是按照申請鎖的時間前后給等 待執行緒分配鎖的,每當鎖被釋放后,任何一個執行緒都有機會競爭到鎖, 這樣做的目的是為了提高執行性能,缺點是可能會產生執行緒饑餓現象,

問題六:什么是鎖消除和鎖粗化?

鎖消除:指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在共享資料競爭的鎖進行消除,
主要根據逃逸分析,
程式員怎么會在明知道不存在資料競爭的情況下使用同步呢?
很多不是程式員自己加入的,

鎖粗化:原則上,同步塊的作用范圍要盡量小,但是如果一系列的連續操作都對同一個物件反復加鎖和解鎖,甚至加鎖操作在回圈體內,頻繁地進行互斥同步操作也會導致不必要的性能損耗,
鎖粗化就是增大鎖的作用域,

問題七:為什么說 Synchronized 是一個悲觀鎖?樂觀鎖的實作原理 又是什么?什么是 CAS,它有什么特性?

Synchronized 顯然是一個悲觀鎖,
因為它的并發策略是悲觀的: 不管是否會產生競爭,任何的資料操作都必須要加鎖、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的執行緒需要被喚醒等操作,
隨著硬體指令集的發展,我們可以使用基于沖突檢測的樂觀并發策略,
先進行操作,如果沒有其他執行緒征用資料,那操作就成功了; 如果共享資料有征用,產生了沖突,那就再進行其他的補償措施,
這種樂觀的并發策略的許多實作不需要執行緒掛起,所以被稱為非阻塞同步,
樂觀鎖的核心演算法是 CAS(Compareand Swap,比較并交換),它涉及到三個運算元:記憶體值、預期值、新值,
當且僅當預期值和記憶體值相等時才將記憶體值修改為新值,
這樣處理的邏輯是,首先檢查某塊記憶體的值是否跟之前我讀取時的一樣,如不一樣則表示期間此記憶體值已經被別的執行緒更改過,舍棄本次操 作,否則說明期間沒有其他執行緒對此記憶體值操作,可以把新值設定給此塊記憶體,
CAS 具有原子性, 它的原子性由 CPU 硬體指令實作保證,即使用 JNI 呼叫 Native 方法呼叫由 C++ 撰寫的硬體級別指令,JDK 中提供了 Unsafe 類執行這些操作,

問題八:樂觀鎖一定就是好的嗎?

樂觀鎖避免了悲觀鎖獨占物件的現象,同時也提高了并發性能,但它也 有缺點:
1.樂觀鎖只能保證一個共享變數的原子操作,如果多一個或幾個變數,樂 觀鎖將變得力不從心,但互斥鎖能輕易解決,不管物件數量多少及物件 顆粒度大小,
2.長時間自旋可能導致開銷大,假如 CAS 長時間不成功而一直自旋,會 給 CPU 帶來很大的開銷,
3.ABA 問題,CAS 的核心思想是通過比對記憶體值與預期值是否一樣而判 斷記憶體值是否被改過,但這個判斷邏輯不嚴謹,假如記憶體值原來是 A, 后來被一條執行緒改為 B,最后又被改成了 A,則 CAS 認為此記憶體值并 沒有發生改變,但實際上是有被其他執行緒改過的,這種情況對依賴程序 值的情景的運算結果影響很大,解決的思路是引入版本號,每次變數更 新都把版本號加一,

可重入鎖 ReentrantLock 及其他顯式鎖相關問題

問題一:跟 Synchronized 相比,可重入鎖 ReentrantLock 其實作 原理有什么不同?

其實,鎖的實作原理基本是為了達到一個目的: 讓所有的執行緒都能看到某種標記,
Synchronized 通過在物件頭中設定標記實作了這一目的,是一種 JVM 原生的鎖實作方式,而 ReentrantLock 以及所有的基于 Lock 介面的實作類,都是通過用一個 volitile 修飾的 int 型變數,并保證每個執行緒都能擁有對該 int 的可見性和原子修改,其本質是基于所謂的 AQS 框架,

問題二:那么請談談 AQS 框架是怎么回事兒?

AQS(AbstractQueuedSynchronizer 類)是一個用來構建鎖和同步器的框架,各種 Lock 包中的鎖(常用的有 ReentrantLock、 ReadWriteLock), 以及其他如 Semaphore、CountDownLatch, 甚至是早期的 FutureTask 等,都是基于 AQS 來構建,

1. AQS 在內部定義了一個 volatile int state 變數,表示同步狀態:當執行緒呼叫 lock 方法時 ,如果 state=0,說明沒有任何執行緒占有共享資源的鎖,可以獲得鎖并將 state=1;如果 state=1,則說明有執行緒目前正在使用共享變數,其他執行緒必須加入同步佇列進行等待,
2. AQS 通過 Node 內部類構成的一個雙向鏈表結構的同步佇列,來完成執行緒獲取鎖的排隊作業,當有執行緒獲取鎖失敗后,就被添加到佇列末尾,
	Node 類是對要訪問同步代碼的執行緒的封裝,包含了執行緒本身及其狀態叫waitStatus(有五種不同 取值,分別表示是否被阻塞,是否等待喚醒, 是否已經被取消等),每個 Node 結點關聯其 prev 結點和 next 結點,方便執行緒釋放鎖后快速喚醒下一個在等待的執行緒,是一個 FIFO 的程序,
	Node 類有兩個常量,SHARED 和 EXCLUSIVE,分別代表共享模式和獨占模式,所謂共享模式是一個鎖允許多條執行緒同時操作(信號量 Semaphore 就是基于 AQS 的共享模式實作的),獨占模式是同一個時間段只能有一個執行緒對共享資源進行操作,多余的請求執行緒需要排隊等待 (如 ReentranLock) ,
3. AQS 通過內部類 ConditionObject 構建等待佇列(可有多個),當 Condition 呼叫 wait() 方法后,執行緒將會加入等待佇列中,而當Condition 呼叫 signal() 方法后,執行緒將從等待佇列轉移動同步佇列中進行鎖競爭,
4. AQS 和 Condition 各自維護了不同的佇列,在使用 Lock 和 Condition 的時候,其實就是兩個佇列的互相移動,

問題三:請盡可能詳盡地對比下 Synchronized 和 ReentrantLock 的異同,

ReentrantLock 是 Lock 的實作類,是一個互斥的同步鎖,
從功能角度,ReentrantLock 比 Synchronized 的同步操作更精細(因為可以像普通物件一樣使用),甚至實作 Synchronized 沒有的高級功能,如:
	等待可中斷:當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可 以選擇放棄等待,對處理執行時間非常長的同步塊很有用,
	帶超時的獲取鎖嘗試:在指定的時間范圍內獲取鎖,如果時間到了仍然 無法獲取則回傳,
	可以判斷是否有執行緒在排隊等待獲取鎖,
	可以回應中斷請求:與 Synchronized 不同,當獲取到鎖的執行緒被中斷時,能夠回應中斷,中斷例外將會被拋出,同時鎖會被釋放,
	可以實作公平鎖,

從鎖釋放角度,Synchronized 在 JVM 層面上實作的,不但可以通過一些監控工具監控 Synchronized 的鎖定,而且在代碼執行出現例外時,JVM 會自動釋放鎖定;
但是使用 Lock 則不行,Lock 是通過代碼實作的,要保證鎖定一定會被釋放,就必須將 unLock() 放到 finally{} 中,
從性能角度,Synchronized 早期實作比較低效,對比 ReentrantLock,大多數場景性能都相差較大,
但是在 Java 6 中對其進行了非常多的改進,在競爭不激烈時,Synchronized 的性能要優于 ReetrantLock;
在高競爭情況下,Synchronized 的性能會下降幾十倍,但是 ReetrantLock 的性能能維持常態,

問題四:ReentrantLock 是如何實作可重入性的?

ReentrantLock 內部自定義了同步器 Sync(Sync 既實作了 AQS,又實作了 AOS,而 AOS 提供了一種互斥鎖持有的方式),其實就是加鎖的時候通過 CAS 演算法,將執行緒物件放到一個雙向鏈表中,每次獲取鎖的時候,看下當前維護的那個執行緒 ID 和當前請求的執行緒 ID 是否一樣,一樣就可重入了,

問題五:除了 ReetrantLock,你還接觸過 JUC 中的哪些并發工具?

通常所說的并發包(JUC)也就是 java.util.concurrent 及其子包,集中了 Java 并發的各種基礎工具類,具體主要包括幾個方面:
	提供了 CountDownLatch、CyclicBarrier、Semaphore 等 ,比 Synchronized 更加高級,可以實作更加豐富多執行緒操作的同步結構,
	提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通過類似快斬訓制實作執行緒安全的動態陣列 CopyOnWriteArrayList 等,各種執行緒安全的容器,
	提供了 ArrayBlockingQueue、SynchorousQueue 或針對特定場景的PriorityBlockingQueue 等,各種并發佇列實作,
	強大的 Executor 框架,可以創建各種不同型別的執行緒池,調度任務運行等,

問題六:請談談 ReadWriteLock 和 StampedLock,

雖然 ReentrantLock 和 Synchronized 簡單實用,但是行為上有一定局限性,要么不占,要么獨占,
實際應用場景中,有時候不需要大量競爭的寫操作,而是以并發讀取為主,為了進一步優化并發操作的粒度,Java提供了讀寫鎖,
讀寫鎖基于的原理是多個讀操作不需要互斥,如果讀鎖試圖鎖定時,寫鎖是被某個執行緒持有,讀鎖將無法獲得,而只好等待對方操作結束,這樣就可以自動保證不會讀取到有爭議的資料,
ReadWriteLock 代表了一對鎖,下面是一個基于讀寫鎖實作的資料結構,當資料量較大,并發讀多、并發寫少的時候,能夠比純同步版本凸顯出優勢:

image

讀寫鎖看起來比 Synchronized 的粒度似乎細一些,但在實際應用中,其表現也并不盡如人意,主要還是因為相對比較大的開銷,
所以,JDK 在后期引入了 StampedLock,在提供類似讀寫鎖的同時,還支持優化讀模式,
優化讀基于假設,大多數情況下讀操作并不會和寫操作沖突,其邏輯是先試著修改,然后通過 validate 方法確認是否進入了寫模式,如果沒有進入,就成功避免了開銷;
如果進入,則嘗試獲取讀鎖,

image

問題七:如何讓 Java 的執行緒彼此同步?你了解過哪些同步器?請分別介紹下,

JUC 中的同步器三個主要的成員: CountDownLatch、CyclicBarrier 和 Semaphore,通過它們可以方便地實作很多執行緒之間協作的功能,
CountDownLatch 叫倒計數,允許一個或多個執行緒等待某些操作完成,
看幾個場景:
跑步比賽,裁判需要等到所有的運動員("其他執行緒")都跑到終點 ("達到目標"),才能去算排名和頒獎,
模擬并發,我需要啟動 100 個執行緒去同時訪問某一個地址,我希望它們能同時并發,而不是一個一個的去執行,
用法: CountDownLatch 構造方法指明計數數量,被等待執行緒呼叫 countDown 將計數器減 1,等待執行緒使用 await 進行執行緒等待,
一個簡單的例子:

image

CyclicBarrier 叫回圈柵欄,它實作讓一組執行緒等待至某個狀態之后再全部同時執行,而且當所有等待執行緒被釋放后,CyclicBarrier 可以被重復使用,
CyclicBarrier 的典型應用場景是用來等待并發執行緒結束,
CyclicBarrier 的主要方法是 await(),await() 每被呼叫一次,計數便會減少 1,并阻塞住當前執行緒,
當計數減至 0 時,阻塞解除,所有在 此 CyclicBarrier 上面阻塞的執行緒開始運行,
在這之后,如果再次呼叫 await(),計數就又會變成 N-1,新一輪重新開始,這便是 Cyclic 的含義所在,
CyclicBarrier.await() 帶有回傳值,用來表示當前執行緒是第幾個到達這個 Barrier 的執行緒,
舉例說明如下:

image

Semaphore,Java 版本的信號量實作,用于控制同時訪問的執行緒個數,來達到限制通用資源訪問的目的,其原理是通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可,

image

如果 Semaphore 的數值被初始化為 1,那么一個執行緒就可以通過 acquire 進入互斥狀態,本質上和互斥鎖是非常相似的,
但是區別也非常明顯,比如互斥鎖是有持有者的,而對于 Semaphore 這種計數器結構,雖然有類似功能,但其實不存在真正意義的持有者,除非我們進行擴展包裝,

問題八:CyclicBarrier 和 CountDownLatch 看起來很相似,請對比下呢?

它們的行為有一定相似度,區別主要在于:
CountDownLatch 是不可以重置的,所以無法重用,CyclicBarrier 沒有這種限制,可以重用,
CountDownLatch 的基本操作組合是 countDown/await,調 用 await 的執行緒阻塞等待 countDown 足夠的次數,不管你是在一個執行緒還是多個執行緒里 countDown,只要次數足夠即可,
CyclicBarrier 的基本操作組合就是 await,當所有的伙伴都呼叫了 await,才會繼續進行任務,并自動進行重置,
CountDownLatch 目的是讓一個執行緒等待其他 N 個執行緒達到某個條件后,自己再去做某個事(通過 CyclicBarrier 的第二個構造方法 public CyclicBarrier(int parties, Runnable barrierAction),在新執行緒里做事可以達到同樣的效果),
而 CyclicBarrier 的目的是讓 N 多執行緒互相等待直到所有的都達到某個狀態,然后這 N 個執行緒再繼續執行各自后續(通過 CountDownLatch 在某些場合也能完成類似的效果),

Java 執行緒池相關問題

問題一:Java 中的執行緒池是如何實作的?

在 Java 中,所謂的執行緒池中的 "執行緒",其實是被抽象為了一個靜態內部類 Worker,它基于 AQS 實作,存放在執行緒池的 HashSet<Worker> workers 成員變數中;
而需要執行的任務則存放在成員變數 workQueue(BlockingQueue<Runnable> workQueue)中,
這樣,整個執行緒池實作的基本思想就是:
從 workQueue 中不斷取出需要執行的任務,放在 Workers 中進行處理,

問題二:創建執行緒池的幾個核心構造引數?

Java 中的執行緒池的創建其實非常靈活,我們可以通過配置不同的引數,創建出行為不同的執行緒池,這幾個引數包括:
corePoolSize:執行緒池的核心執行緒數,
maximumPoolSize:執行緒池允許的最大執行緒數,
keepAliveTime:超過核心執行緒數時閑置執行緒的存活時間,
workQueue:任務執行前保存任務的佇列,保存由 execute 方法提交的 Runnable 任務,

問題三:執行緒池中的執行緒是怎么創建的?是一開始就隨著執行緒池的啟動創建好的嗎?

顯然不是的,
執行緒池默認初始化后不啟動 Worker,等待有請求時才啟動,
每當我們呼叫 execute() 方法添加一個任務時,執行緒池會做如下判斷:
如果正在運行的執行緒數量小于 corePoolSize,那么馬上創建執行緒運行這個任務;
如果正在運行的執行緒數量大于或等于 corePoolSize,那么將這個任務 放入佇列;
如果這時候佇列滿了,而且正在運行的執行緒數量小于 maximumPoolSize,那么還是要創建非核心執行緒立刻運行這個任務;
如果佇列滿了,而且正在運行的執行緒數量大于或等于 maximumPoolSize,那么執行緒池會拋出例外 RejectExecutionException,
當一個執行緒完成任務時,它會從佇列中取下一個任務來執行,
當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷,
如果當前運行的執行緒數大于 corePoolSize,那么這個執行緒就被停掉,
所以執行緒池的所有任務完成后,它最侄訓收縮到 corePoolSize 的大小,

問題四:既然提到可以通過配置不同引數創建出不同的執行緒池,那么 Java 中默認實作好的執行緒池又有哪些呢?請比較它們的異同,

1. SingleThreadExecutor 執行緒池
這個執行緒池只有一個核心執行緒在作業,也就是相當于單執行緒串行執行所有任務,
如果這個唯一的執行緒因為例外結束,那么會有一個新的執行緒來替代它,
此執行緒池保證所有任務的執行順序按照任務的提交順序執行,
corePoolSize: 1,只有一個核心執行緒在作業,
maximumPoolSize: 1,
keepAliveTime: 0L,
workQueue: new LinkedBlockingQueue<Runnable>(),其緩沖佇列是無界的,

2. FixedThreadPool 執行緒池
FixedThreadPool 是固定大小的執行緒池,只有核心執行緒,
每次提交一個 任務就創建一個執行緒,直到執行緒達到執行緒池的最大大小,
執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行例外而結束,那么執行緒池會補充一個新執行緒,
FixedThreadPool 多數針對一些很穩定很固定的正規并發執行緒,多用于服務器,
corePoolSize: nThreads
maximumPoolSize: nThreads
keepAliveTime: 0L
workQueue:new LinkedBlockingQueue<Runnable>(),其緩沖佇列是無界的,
3. CachedThreadPool 執行緒池
CachedThreadPool 是無界執行緒池,如果執行緒池的大小超過了處理任務所需要的執行緒,那么就會回收部分空閑( 60 秒不執行任務)執行緒,當任務數增加時,此執行緒池又可以智能的添加新執行緒來處理任務,
執行緒池大小完全依賴于作業系統(或者說 JVM)能夠創建的最大執行緒大小,
SynchronousQueue 是一個是緩沖區為 1 的阻塞佇列,
快取型池子通常用于執行一些生存期很短的異步型任務,因此在一些面向連接的 daemon 型 SERVER 中用得不多,
但對于生存期短的異步任務,它是 Executor 的首選,
corePoolSize: 0
maximumPoolSize: Integer.MAX_VALUE
keepAliveTime: 60L
workQueue:new SynchronousQueue<Runnable>(),一個是緩沖區為 1 的阻塞佇列,
4. ScheduledThreadPool 執行緒池
ScheduledThreadPool: 核心執行緒池固定,大小無限的執行緒池,
此執行緒池支持定時以及周期性執行任務的需求,
創建一個周期性執行任務的執行緒池,
如果閑置,非核心執行緒池會在 DEFAULT_KEEPALIVEMILLIS 時間內回收,
corePoolSize: corePoolSize
maximumPoolSize: Integer.MAX_VALUE
keepAliveTime: DEFAULT_KEEPALIVE_MILLIS
workQueue:new DelayedWorkQueue()

問題五:如何在 Java 執行緒池中提交執行緒?

執行緒池最常用的提交任務的方法有兩種:
1. execute(): ExecutorService.execute 方法接收一個例,它用來執行一個任務:

image

2. submit(): ExecutorService.submit() 方法回傳的是 Future 物件,
可以用 isDone() 來查詢 Future 是否已經完成,當任務完成時,它具有一個結果,可以呼叫 get() 來獲取結果,
也可以不用 isDone() 進行檢查就直接呼叫 get(),在這種情況下,get() 將阻塞,直至結果準備就緒,

image

Java 記憶體模型相關問題

問題一:什么是 Java 的記憶體模型,Java 中各個執行緒是怎么彼此看到對方的變數的?

Java 的記憶體模型定義了程式中各個變數的訪問規則,即在虛擬機中將變數存盤到記憶體和從記憶體中取出這樣的底層細節,
此處的變數包括實體欄位、靜態欄位和構成陣列物件的元素,但是不包括區域變數和方法引數,因為這些是執行緒私有的,不會被共享,所以不存在競爭問題,

Java 中各個執行緒是怎么彼此看到對方的變數的呢?
Java 中定義了主記憶體與作業記憶體的概念:
所有的變數都存盤在主記憶體,每條執行緒還有自己的作業記憶體,保存了被該執行緒使用到的變數的主記憶體副本拷貝,
執行緒對變數的所有操作(讀取、賦值)都必須在作業記憶體中進行,不能\直接讀寫主記憶體的變數,
不同的執行緒之間也無法直接訪問對方作業記憶體的變數,執行緒間變數值的傳遞需要通過主記憶體,

問題二:請談談 volatile 有什么特點,為什么它能保證變數對所有線 程的可見性?

關鍵字 volatile 是 Java 虛擬機提供的最輕量級的同步機制,
當一個變數被定義成 volatile 之后,具備兩種特性:
1. 保證此變數對所有執行緒的可見性,
當一條執行緒修改了這個變數的值,新值對于其他執行緒是可以立即得知的,
而普通變數做不到這一點,
2. 禁止指令重排序優化,
普通變數僅僅能保證在該方法執行程序中,得到正確結果,但是不保證程式代碼的執行順序,

Java 的記憶體模型定義了 8 種記憶體間操作:
lock 和 unlock
把一個變數標識為一條執行緒獨占的狀態,
把一個處于鎖定狀態的變數釋放出來,釋放之后的變數才能被其他執行緒鎖定,
read 和 write
把一個變數值從主記憶體傳輸到執行緒的作業記憶體,以便 load,
把 store 操作從作業記憶體得到的變數的值,放入主記憶體的變數中,
load 和 store
把 read 操作從主記憶體得到的變數值放入作業記憶體的變數副本中,
把作業記憶體的變數值傳送到主記憶體,以便 write,
use 和 assgin
把作業記憶體變數值傳遞給執行引擎,
將執行引擎值傳遞給作業記憶體變數值,

volatile 的實作基于這 8 種記憶體間操作,保證了一個執行緒對某個 volatile 變數的修改,一定會被另一個執行緒看見,即保證了可見性,

問題三:既然 volatile 能夠保證執行緒間的變數可見性,是不是就意味著基于volatile 變數的運算就是并發安全的?

顯然不是的,
基于 volatile 變數的運算在并發下不一定是安全的,
volatile 變數在各個執行緒的作業記憶體,不存在一致性問題(各個執行緒的作業記憶體中 volatile 變數,每次使用前都要重繪到主記憶體),
但是 Java 里面的運算并非原子操作,導致 volatile 變數的運算在并發下一樣是不安全的,

問題四:請對比下 volatile 對比 Synchronized 的異同,

Synchronized 既能保證可見性,又能保證原子性,而 volatile 只能保證可見性,無法保證原子性,
ThreadLocal 和 Synchonized 都用于解決多執行緒并發訪問,防止任務在共享資源上產生沖突,
但是 ThreadLocal 與 Synchronized 有本質的區別,
Synchronized 用于實作同步機制,是利用鎖的機制使變數或代碼塊在某一時該只能被一個執行緒訪問,是一種  "以時間換空間"  的方式,
而 ThreadLocal 為每一個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的并不是同一個物件,根除了對變數的共享,是一種 "以空間換時間" 的方式,

問題五:請談談 ThreadLocal 是怎么解決并發安全的?

ThreadLocal 這是 Java 提供的一種保存執行緒私有資訊的機制,因為其在整個執行緒生命周期內有效,所以可以方便地在一個執行緒關聯的不同業務模塊之間傳遞資訊,比如事務 ID、Cookie 等背景關系相關資訊,
ThreadLocal 為每一個執行緒維護變數的副本,把共享資料的可見范圍限制在同一個執行緒之內,其實作原理是,在 ThreadLocal 類中有一個 Map,用于存盤每一個執行緒的變數的副本,

問題六:很多人都說要慎用 ThreadLocal,談談你的理解,使用 ThreadLocal 需要注意些什么?

使 用 ThreadLocal 要 注 意 remove!
ThreadLocal 的實作是基于一個所謂的 ThreadLocalMap,在 ThreadLocalMap 中,它的 key 是一個弱參考,
通常弱參考都會和參考佇列配合清理機制使用,但是 ThreadLocal 是個例外,它并沒有這么做,
這意味著,廢棄專案的回收依賴于顯式地觸發,否則就要等待執行緒結束,進而回收相應 ThreadLocalMap!
這就是很多 OOM 的來源,所以通常都會建議,應用一定要自己負責 remove,并且不要和執行緒池配合,因為 worker 執行緒往往是不會退出的,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/390220.html

標籤:Java

上一篇:如何將字典中的多個值添加到PySpark資料幀

下一篇:Java連載155-IO總結(二)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more