三、并發編程高級面試專欄
1、Synchronized用過嗎?其原理是什么2
這是一道Java面試中幾乎百分百會問到的問題,因為沒有任何寫過并發程序的開發者會沒聽說或者沒接觸過Synchronized。Synchronized是由JVM 實現的一種實現互斥同步的一種方式,如果你查看被Synchronized修飾過的程序塊編譯后的字節碼,會發現,被Synchronized修飾過的程序塊,在 編譯前后被編譯器生成了monitorenter和monitorexit兩個字節碼指令。這兩 個指令是什么意思呢2在虛擬機執行到monitorenter指令時,首先要嘗試獲取對象的鎖:如果這個對象沒有鎖定,或者當前線程已經擁有了這個對 象的鎖,把鎖的計數器+ 1;當執行monitorexit指令時將鎖計數器-1;當計數器為0時,鎖就被釋放了。如果獲取對象失敗了,那當前線程就要阻塞 等待,直到對象鎖被另外一個線程釋放為止。Java中Synchronize通過在對 象頭設置標記,達到了獲取鎖和釋放鎖的目的。
2、你剛才提到獲取對象的鎖?這個 “鎖 ”到底是什么2如何確定對象的鎖2
“鎖 ”的本質其實是monitorenter和monitorexit字節碼指令的一個Reference 類型的參數,即要鎖定和解鎖的對象。我們知道,使用Synchronized 可以修飾不同的對象,因此,對應的對象鎖可以這么確定。
1.如果 Synchronized 明確指定了鎖對象,比如 Synchronized(變量名)、Synchronized(this) 等,說明加解鎖對象為該對象。
2.如果沒有明確指定:
若 Synchronized 修飾的方法為非靜態方法,表示此方法對應的對象為 鎖對象;
若 Synchronized 修飾的方法為靜態方法,則表示此方法對應的類對象 為鎖對象。
注意,當一個對象被鎖住時,對象里面所有用 Synchronized 修飾的 方法都將產生堵塞,而對象里非 Synchronized 修飾的方法可正常被 調用,不受鎖影響。
3、什么是可重入性?為什么說 Synchronized 是可重入鎖?
可重入性是鎖的一個基本要求,是為了解決自己鎖死自己的情況。 比如下面的偽代碼,一個類中的同步方法調用另一個同步方法,假如Synchronized 不支持重入,進入 method2 方法時當前線程獲得鎖,
method2 方法里面執行 method1 時當前線程又要去嘗試獲取鎖,這 時如果不支持重入,它就要等釋放,把自己阻塞,導致自己鎖死自己。
對 Synchronized 來說,可重入性是顯而易見的,剛才提到,在執行monitorenter 指令時,如果這個對象沒有鎖定,或者當前線程已經擁有了這個對象的鎖(而不是已擁有了鎖則不能繼續獲取),就把鎖的計 數器 +1, 其實本質上就通過這種方式實現了可重入性。
4、JVM 對 Java 的原生鎖做了哪些優化?
在 Java 6 之前,Monitor 的實現完全依賴底層操作系統的互斥鎖來 實現, 也就是我們剛才在問題二中所闡述的獲取/釋放鎖的邏輯。
由于 Java 層面的線程與操作系統的原生線程有映射關系,如果要將一 個
線程進行阻塞或喚起都需要操作系統的協助,這就需要從用戶態切換 到內核態來執行,這種切換代價十分昂貴,很耗處理器時間,現代 JDK 中做了大量的優化。 一種優化是使用自旋鎖,即在把線程進行阻塞操作之前先讓線程自旋等 待一段時間,可能在等待期間其他線程已經解鎖,這時就無需再讓線程 執行阻塞操作,避免了用戶態到內核態的切換。
現代 JDK 中還提供了三種不同的 Monitor 實現,也就是三種不同的 鎖:
- 偏向鎖(Biased Locking)
- 輕量級鎖
- 重量級鎖
這三種鎖使得 JDK 得以優化 Synchronized 的運行,當 JVM 檢測 到不同的競爭狀況時,會自動切換到適合的鎖實現,這就是鎖的升級、 降級。 - 當沒有競爭出現時,默認會使用偏向鎖。
JVM 會利用 CAS 操作,在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向于當前線程,所以并不涉及真正的互斥鎖,因 為在很多應用場景中,大部分對象生命周期中最多會被一個線程鎖定, 使用偏斜鎖可以降低無競爭開銷。 - 如果有另一線程試圖鎖定某個被偏斜過的對象,JVM 就撤銷偏斜鎖, 切換到輕量級鎖實現。
- 輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功, 就使用普通的輕量級鎖;否則,進一步升級為重量級鎖。
5、為什么說 Synchronized 是非公平鎖?
非公平主要表現在獲取鎖的行為上,并非是按照申請鎖的時間前后給等 待線程分配鎖的,每當鎖被釋放后,任何一個線程都有機會競爭到鎖, 這樣做的目的是為了提高執行性能,缺點是可能會產生線程饑餓現象。
6、什么是鎖消除和鎖粗化?
- 鎖消除:指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被 檢測到不可能存在共享數據競爭的鎖進行消除。主要根據逃逸分析。
程序員怎么會在明知道不存在數據競爭的情況下使用同步呢?很多不是 程序員自己加入的。 - 鎖粗化:原則上,同步塊的作用范圍要盡量小。但是如果一系列的連續 操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作在循環體內,頻繁 地進行互斥同步操作也會導致不必要的性能損耗。
鎖粗化就是增大鎖的作用域。
7、為什么說 Synchronized 是一個悲觀鎖?樂觀鎖的實現原理 又是什么?什么是 CAS?它有什么特性?
Synchronized 顯然是一個悲觀鎖,因為它的并發策略是悲觀的: 不管是否會產生競爭,任何的數據操作都必須要加鎖、用戶態核心態轉 換、維護鎖計數器和檢查是否有被阻塞的線程需要被喚醒等操作。隨著硬件指令集的發展,我們可以使用基于沖突檢測的樂觀并發策略。先進行操作,如果沒 有其他線程征用數據,那操作就成功了; 如果共享數據有征用,產生了沖突,那就再進行其他的補償措施。這種樂觀的并發策略的許多實現不需要線程掛起,所以被稱為非阻塞同步。樂觀鎖的核心算法是CAS(Compareand Swap,比較并交換),它涉及到三個操作數:內存值、預期值、新值。當且僅當預期值和內存值相等時才將內存值修改為新值。 這樣處理的邏輯是,首先檢查某塊內存的值是否跟之前我讀取時的一 樣, 如不一樣則表示期間此內存值已經被別的線程更改過,舍棄本次操 作,否則說明期間沒有其他線程對此內存值操作,可以把新值設置給此 塊內存。CAS 具有原子性,它的原子性由 CPU 硬件指令實現保證,即使用 JNI 調用 Native 方法調用由 C++ 編寫的硬件級別指令,JDK 中提 供了 Unsafe 類執行這些操作。
8、樂觀鎖一定就是好的嗎?
樂觀鎖避免了悲觀鎖獨占對象的現象,同時也提高了并發性能,但它也有缺點:
1.樂觀鎖只能保證一個共享變量的原子操作。如果多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,不管對象數量多少及對象顆粒度大小。
2.長時間自旋可能導致開銷大。假如 CAS 長時間不成功而一直自旋,會給 CPU 帶來很大的開銷。
3.ABA 問題。CAS 的核心思想是通過比對內存值與預期值是否一樣而判斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是 A, 后來被一條線程改為 B,最后又被改成了 A,則 CAS 認為此內存值并沒有發生改變,但實際上是有被其他線程改過的,這種情況對依賴過程值的情景 的運算結果影響很大。解決的思路是引入版本號,每次變量更新都把版本號加一。
9、跟 Synchronized 相比?可重入鎖 ReentrantLock 其實現 原理有什么不同?
其實,鎖的實現原理基本是為了達到一個目的: 讓所有的線程都能看到某種標記。
Synchronized 通過在對象頭中設置標記實現了這一目的,是一種 JVM 原生的鎖實現方式,而 ReentrantLock 以及所有的基于 Lock 接口的 實現類,都是通過用一個 volitile 修飾的 int 型變量,并保證每個線 程都能擁有對該 int 的可見性和原子修改,其本質是基于所謂的 AQS 框架。
10 、 那 么 請 談 談 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
的時候,其實就是兩個隊列的互相移動。
11、請盡可能詳盡地對比下 Synchronized 和 ReentrantLock 的異同)
ReentrantLock 是 Lock 的實現類,是一個互斥的同步鎖。從功能角度, ReentrantLock 比 Synchronized 的同步操作更精細(因為可以像普通對象一樣使用),甚至實現 Synchronized 沒有的高級功能,如:
- 等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,對處理執行時間非常長的同步塊很有用。
- 帶超時的獲取鎖嘗試:在指定的時間范圍內獲取鎖,如果時間到了仍然無法獲取則返回。
- 可以判斷是否有線程在排隊等待獲取鎖。
- 可以響應中斷請求:與 Synchronized 不同,當獲取到鎖的線程被中斷時,能夠響應中斷,中斷異常將會被拋出,同時鎖會被釋放。
- 可以實現公平鎖。
從鎖釋放角度,Synchronized 在 JVM 層面上實現的,不但可以通過一些 監控工具監控 Synchronized 的鎖定,而且在代碼執行出現異常 時,JVM 會自動釋放鎖定;但是使用 Lock 則不行,Lock 是通過代碼實現的,要保證鎖定一定會被釋放,就必須將 unLock() 放到 finally{} 中 。
從性能角度,Synchronized 早期實現比較低效,對比ReentrantLock,大多數場景性能都相差較大。
但是在 Java 6 中對其進行了非常多的改進,在競爭不激烈時, Synchronized 的性能要優于 ReetrantLock;在高競爭情況下, Synchronized 的性能會下降幾十倍,但是 ReetrantLock 的性能能維持常態。
12、ReentrantLock是如何實現可重入性的?
ReentrantLock 內部自定義了同步器 Sync(Sync 既實現了 AQS, 又實現了 AOS,而 AOS 提供了一種互斥鎖持有的方式),其實就是 加鎖的時候通過CAS 算法,將線程對象放到一個雙向鏈表中,每次獲 取鎖的時候,看下當前維護的那個線程 ID 和當前請求的線程 ID 是否 一樣,一樣就可重入了。
13、除了ReetrantLock?你還接觸過JUC中的哪些并發工具?
通常所說的并發包(JUC)也就是 java.util.concurrent 及其子包,集中了 Java
并發的各種基礎工具類,具體主要包括幾個方面:
提供了 CountDownLatch、CyclicBarrier、Semaphore等,比Synchronized 更加高級,可以實現更加豐富多線程操作的同步結構。
提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通過類似快照機制實現線程安全的動態數組 CopyOnWriteArrayList 等各種線
程安全的容器。
- 提供了 ArrayBlockingQueue、 SynchorousQueue 或針對特定場景的
PriorityBlockingQueue 等,各種并發隊列實現。
- 強大的 Executor 框架,可以創建各種不同類型的線程池,調度任務運行等。
14、請談談ReadWriteLock和StampedLock)
雖然 ReentrantLock 和 Synchronized 簡單實用,但是行為上有一定局限性,要么不占,要么獨占。實際應用場景中,有時候不需要大量 競爭的寫操作,而是以并發讀取為主,為了進一步優化并發操作的粒 度,Java 提供了讀寫鎖。讀寫鎖基于的原理是多個讀操作不需要互斥,如果讀鎖試圖鎖定時,寫鎖是被某個線程持有,讀鎖將無法獲得,而只好等待對方操作 結束,這樣就可以自動保證不會讀取到有爭議的數據。 ReadWriteLock 代表了一對鎖,下面是一個基于讀寫鎖實現的數據結 構, 當數據量較大,并發讀多、并發寫少的時候,能夠比純同步版本凸 顯出優勢:
讀寫鎖看起來比 Synchronized 的粒度似乎細一些,但在實際應用 中,其表現也并不盡如人意,主要還是因為相對比較大的開銷。所以,JDK 在后期引入了 StampedLock,在提供類似讀寫鎖的同時,還支持優化讀模式。優化讀基于假設,大多數情況下讀操作并不會和寫 操作沖突,其邏輯是先試著修改,然后通過 validate 方法確認是否進入了寫模式,如果沒有進入,就成功避免了開銷;如果進入,則嘗試獲取讀鎖。
15、如何讓Java的線程彼此同步?你了解過哪些同步器?請分別 介紹下)
JUC 中的同步器三個主要的成員:CountDownLatch、CyclicBarrier 和Semaphore,通過它們可以方便地實現很多線程之間協作的功能。CountDownLatch 叫倒計數,允許一個或多個線程等待某些操作完成。看幾個場景:
跑步比賽,裁判需要等到所有的運動員(“其他線程”)都跑到終點 (達到目標),才能去算排名和頒獎。
模擬并發,我需要啟動 100 個線程去同時訪問某一個地址,我希望它 們能同時并發,而不是一個一個的去執行。
用法:CountDownLatch 構造方法指明計數數量,被等待線程調用countDown 將計數器減 1,等待線程使用 await 進行線程等待。一 個簡單的例子:
CyclicBarrier 叫循環柵欄,它實現讓一組線程等待至某個狀態之后再全部同時執行,而且當所有等待線程被釋放后,CyclicBarrier 可以被 重復使用。CyclicBarrier 的典型應用場景是用來等待并發線程結束。CyclicBarrier 的主要方法是 await(),await() 每被調用一次,計數便 會減少 1,并阻塞住當前線程。當計數減至 0 時,阻塞解除,所有在 此 CyclicBarrier 上面阻塞的線程開始運行。
在這之后,如果再次調用 await(),計數就又會變成 N-1,新一輪重新開始,這便是 Cyclic 的含義所在。CyclicBarrier.await() 帶有返回值,用來表示當前線程是第幾個到達這個 Barrier 的線程。
舉例說明如下:
Semaphore,Java 版本的信號量實現,用于控制同時訪問的線程個數,來達到限制通用資源訪問的目的,其原理是通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。
如果 Semaphore 的數值被初始化為1,那么一個線程就可以通過 acquire
進入互斥狀態,本質上和互斥鎖是非常相似的。但是區別也非常明顯,比
如互斥鎖是有持有者的,而對于 Semaphore 這種計數器結構,雖然有類似功能,但其實不存在真正意義的持有者,除非我們進行擴展包裝。
16、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 在某些場合也能完成類似的效果)。
17、Java中的線程池是如何實現的?
? 在 Java 中,所謂的線程池中的“線程”,其實是被抽象為了一個靜態 內部類 Worker,它基于 AQS 實現,存放在線程池的
HashSet workers 成員變量中;
? 而需要執行的任務則存放在成員變量 workQueue(BlockingQueue workQueue)中。
這樣,整個線程池實現的基本思想就是:從 workQueue 中不斷取出 需要執行的任務,放在 Workers 中進行處理。
18、創建線程池的幾個核心構造參數?
Java 中的線程池的創建其實非常靈活,我們可以通過配置不同的參 數, 創建出行為不同的線程池,這幾個參數包括:
corePoolSize:線程池的核心線程數。
maximumPoolSize:線程池允許的最大線程數。
keepAliveTime:超過核心線程數時閑置線程的存活時間。
workQueue:任務執行前保存任務的隊列,保存由 execute 方法提交的
Runnable 任務 。
19、線程池中的線程是怎么創建的?是一開始就隨著線程池的啟動 創建好的嗎?
顯然不是的。線程池默認初始化后不啟動 Worker,等待有請求時才啟動。
每當我們調用 execute() 方法添加一個任務時,線程池會做如下判 斷:
? 如果正在運行的線程數量小于 corePoolSize,那么馬上創建線程運行這個任務;
? 如果正在運行的線程數量大于或等于 corePoolSize,那么將這個任務放入隊列;
? 如果這時候隊列滿了,而且正在運行的線程數量小于
maximumPoolSize,那么還是要創建非核心線程立刻運行這個任務;
? 如果隊列滿了,而且正在運行的線程數量大于或等于 maximumPoolSize,那么線程池會拋出異常 RejectExecutionException。當一個線程完成任務時,它會從隊列中取下一個任務來執行。 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷。
如果當前運行的線程數大于 corePoolSize,那么這個線程就被停掉。所以線程池的所有任務完成后,它最終會收縮到 corePoolSize 的大小。
20、既然提到可以通過配置不同參數創建出不同的線程池?那么Java中默認實現好的線程池又有哪些呢?請比較它們的異同)
- SingleThreadExecutor 線程池
這個線程池只有一個核心線程在工作,也就是相當于單線程串行執行所 有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來 替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
corePoolSize:1,只有一個核心線程在工作。
maximumPoolSize: 1。
keepAliveTime: 0L。
[圖片上傳失敗...(image-5ce26f-1607865457378)] ?
是無界的。
- FixedThreadPool 線程池
,其緩沖隊列
FixedThreadPool 是固定大小的線程池,只有核心線程。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。
FixedThreadPool 多數針對一些很穩定很固定的正規并發線程,多用于服
務器。
corePoolSize: nThreads
maximumPoolSize: nThreads
keepAliveTime: 0L
- 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()
21、如何在Java線程池中提交線程?
線程池最常用的提交任務的方法有兩種:
-
execute(): ExecutorService.execute 方法接收一個例,它用來執行一個任務:
image.png -
submit(): ExecutorService.submit() 方法返回的是 Future 對象。可以用 isDone() 來查詢 Future 是否已經完成,當任務完成時, 它具有一個結果, 可以調用 get() 來獲取結果。也可以不用 isDone() 進行檢查就直接調用get(),在這種情況下,get() 將阻塞,直至結果準備就緒。
image.png
22、什么是Java的內存模型?Java中各個線程是怎么彼此看到 對方的變量的?
Java 的內存模型定義了程序中各個變量的訪問規則,即在虛擬機中將 變量存儲到內存和從內存中取出這樣的底層細節。此處的變量包括實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量和方法參數, 因為這些是線程私有的,不會被共享,所以不存在競爭問題。
Java 中各個線程是怎么彼此看到對方的變量的呢?Java 中定義了主內 存與工作內存的概念:
所有的變量都存儲在主內存,每條線程還有自己的工作內存,保存了被 該線程使用到的變量的主內存副本拷貝。
線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,不能直接讀寫主內存的變量。不同的線程之間也無法直接訪問對方工作內存的變量,線程間變量值的傳遞需要通過主內存。
23、請談談volatile有什么特點?為什么它能保證變量對所有線 程的可見性?
關鍵字 volatile 是 Java 虛擬機提供的最輕量級的同步機制。當一個 變量被定義成 volatile 之后,具備兩種特性:
- 保證此變量對所有線程的可見性。當一條線程修改了這個變量的值,新值對于其他線程是可以立即得知的。而普通變量做不到這一點。
- 禁止指令重排序優化。普通變量僅僅能保證在該方法執行過程中,得到正確結果,但是不保證程序代碼的執行順序。
Java 的內存模型定義了 8 種內存間操作:
lock和unlock
- 把一個變量標識為一條線程獨占的狀態。
- 把一個處于鎖定狀態的變量釋放出來,釋放之后的變量才能被其他線程鎖定。
read和write
- 把一個變量值從主內存傳輸到線程的工作內存,以便 load。
- 把 store 操作從工作內存得到的變量的值,放入主內存的變量中。
load和store
- 把 read 操作從主內存得到的變量值放入工作內存的變量副本中。 ? 把工作內存的變量值傳送到主內存,以便 write。
use和assgin
- 把工作內存變量值傳遞給執行引擎。
- 將執行引擎值傳遞給工作內存變量值。
volatile 的實現基于這 8 種內存間操作,保證了一個線程對某個 volatile 變量的修改,一定會被另一個線程看見,即保證了可見性。
24、既然volatile能夠保證線程間的變量可見性?是不是就意味 著基于volatile變量的運算就是并發安全的*?
顯然不是的。基于 volatile 變量的運算在并發下不一定是安全的。 volatile 變量在各個線程的工作內存,不存在一致性問題(各個線程的工作內存中volatile 變量,每次使用前都要刷新到主內存)。
但是 Java 里面的運算并非原子操作,導致 volatile 變量的運算在并發下一樣是不安全的。
25、請對比下volatile對比Synchronized的異同)
Synchronized 既能保證可見性,又能保證原子性,而 volatile 只能保證可見性,無法保證原子性。
ThreadLocal 和 Synchonized 都用于解決多線程并發訪問,防止任務在共享資源上產生沖突。但是 ThreadLocal 與 Synchronized 有本質的區別。Synchronized 用于實現同步機制,是利用鎖的機制使變量或代碼塊在某一時該只能被一個線程訪問,是一種 “以時間換空間” 的方式。
而 ThreadLocal 為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,根除了對變量的共享,是一種 “以空間換時間” 的方式。
26、請談談ThreadLocal是怎么解決并發安全的?
ThreadLocal 這是 Java 提供的一種保存線程私有信息的機制,因為 其在整個線程生命周期內有效,所以可以方便地在一個線程關聯的不同業務模塊之間傳遞信息,比如事務 ID、Cookie 等上下文相關信息。 ThreadLocal 為每一個線程維護變量的副本,把共享數據的可見范圍限 制在同一個線程之內,其實現原理是,在 ThreadLocal 類中有一個 Map,用于存儲每一個線程的變量的副本。
27、很多人都說要慎用ThreadLocal?談談你的理解?使用ThreadLocal需要注意些什么?
使用 ThreadLocal 要注意 remove!
ThreadLocal 的實現是基于一個所謂的 ThreadLocalMap,在ThreadLocalMap 中,它的 key 是一個弱引用。
通常弱引用都會和引用隊列配合清理機制使用,但是 ThreadLocal 是個例外,它并沒有這么做。
這意味著,廢棄項目的回收依賴于顯式地觸發,否則就要等待線程結束, 進而回收相應 ThreadLocalMap!這就是很多 OOM 的來源,所 以通常都會建議,應用一定要自己負責 remove,并且不要和線程池配 合,因為worker 線程往往是不會退出的。