並發編程


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 線程往往是不會退出的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM