虛擬存儲涉及到的相關基礎知識總結 1
810
2025-04-02
volatile 這個關鍵字大家都不陌生,這個關鍵字一般通常用于并發(fā)編程中,是 Java 虛擬機提供的輕量化同步機制,你可能知道 volatile 是干啥的,但是你未必能夠清晰明了的知道 volatile 的實現(xiàn)機制,以及 volatile 解決了什么問題,這篇文章我就來帶大家解析一波。
volatile 能夠保證共享變量之間的 可見性,共享變量是存在堆區(qū)的,而堆區(qū)又與內存模型有關,所以我們要聊 volatile ,就需要首先了解一下 Java 內存模型。Java 中的內存模型是 JVM 提供的,而 JVM 又是和內存進行交互的,所以在聊 Java 內存模型前,我們還需要了解一下操作系統(tǒng)層面中內存模型的相關概念。
先從內存模型談起
計算機在執(zhí)行程序時,會從內存中讀取數(shù)據(jù),然后加載到 CPU 中運行。由于 CPU 執(zhí)行指令的速度要比從內存中讀取和寫入的速度快的多,所以如果每條指令都要和內存交互的話,會大大降低 CPU 的運行速度,造成昂貴的 CPU 性能損耗,為了解決這種問題,設計了 CPU 高速緩存。有了 CPU 高速緩存后,CPU 就不再需要頻繁的和內存交互了,有高速緩存就行了,而 CPU 高速緩存,就是我們經(jīng)常說的 L1 、L2、L3 cache。
當程序在運行過程中,會將運算需要的數(shù)據(jù)從主存復制一份到 CPU 的高速緩存中,在 CPU 進行計算時就可以直接從它的高速緩存讀寫數(shù)據(jù),當運算結束之后,再將高速緩存中的數(shù)據(jù)刷新到主存中。
就拿我們常說的
i = i + 1
1
來舉例子
當 CPU 執(zhí)行這條語句時,會先從內存中讀取 i 的值,復制一份到高速緩存當中,然后 CPU 執(zhí)行指令對 i 進行加 1 操作,再將數(shù)據(jù)寫入高速緩存,最后將高速緩存中 i 最新的值刷新到主存當中。
這個代碼在單線程中運行是沒有任何問題的,但是在多線程中運行就會有問題了,因為每個 CPU 都可以運行一條線程,線程就是程序的順序執(zhí)行流,因此每個線程運行時有自己的高速緩存(對單核 CPU 來說,其實也會出現(xiàn)這種問題,只不過是以線程調度的形式來分別執(zhí)行的)。本文我們以多核 CPU 為例來講解說明。
比如同時有 2 個線程執(zhí)行這段代碼,假如初始時 i 的值為 0,那么我們希望兩個線程執(zhí)行完之后 i 的值變?yōu)?2,但是事實會是這樣嗎?
可能存在下面一種情況:初始時,兩個線程分別讀取 i 的值存入各自所在的 CPU 高速緩存中,然后線程 1 執(zhí)行加 1 操作,把 i 的最新值 1 寫入到內存。此時線程 2 的高速緩存當中 i 的值還是 0,進行加 1 操作之后,i 的值為 1,然后線程 2 把 i 的值寫入內存。
最終結果 i 的值是 1,而不是 2。這就是著名的緩存一致性問題。通常稱這種被多個線程訪問的變量為共享變量。
也就是說,如果一個變量在多個 CPU 中都存在緩存(一般在多線程編程時才會出現(xiàn)),就很可能存在緩存不一致的問題。
Java 內存模型
我們上面說到,共享變量會存在緩存不一致的問題,緩存不一致問題換種說法就是線程安全問題,那么共享變量在 Java 中是如何存在的呢?JVM 中有沒有提供線程安全的變量或者數(shù)據(jù)呢?
這就要聊聊 Java 內存模型的問題了,圖示如下
虛擬機棧 : Java 虛擬機棧是線程私有的數(shù)據(jù)區(qū),Java 虛擬機棧的生命周期與線程相同,虛擬機棧也是局部變量的存儲位置。方法在執(zhí)行過程中,會在虛擬機棧種創(chuàng)建一個 棧幀(stack frame)。
本地方法棧: 本地方法棧也是線程私有的數(shù)據(jù)區(qū),本地方法棧存儲的區(qū)域主要是 Java 中使用 native 關鍵字修飾的方法所存儲的區(qū)域。
程序計數(shù)器:程序計數(shù)器也是線程私有的數(shù)據(jù)區(qū),這部分區(qū)域用于存儲線程的指令地址,用于判斷線程的分支、循環(huán)、跳轉、異常、線程切換和恢復等功能,這些都通過程序計數(shù)器來完成。
方法區(qū):方法區(qū)是各個線程共享的內存區(qū)域,它用于存儲虛擬機加載的 類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
堆: 堆是線程共享的數(shù)據(jù)區(qū),堆是 JVM 中最大的一塊存儲區(qū)域,所有的對象實例都會分配在堆上
運行時常量池:運行時常量池又被稱為 Runtime Constant Pool,這塊區(qū)域是方法區(qū)的一部分,它的名字非常有意思,它并不要求常量一定只有在編譯期才能產(chǎn)生,也就是并非編譯期間將常量放在常量池中,運行期間也可以將新的常量放入常量池中,String 的 intern 方法就是一個典型的例子。
根據(jù)上面的描述可以看到,會產(chǎn)生緩存不一致問題(線程安全問題)的有堆區(qū)和方法區(qū)。而虛擬機棧、本地方法棧、程序計數(shù)器是線程私有,由線程封閉的原因,它們不存在線程安全問題。
針對線程安全問題,有沒有解決辦法呢?
一般情況下,Java 中解決緩存不一致的方法有兩種,第一種就是 synchronized 使用的總線鎖方式,也就是在總線上聲言 LOCK# 信號;第二種就是著名的 MESI 協(xié)議。這兩種都是硬件層面提供的解決方式。
我們先來說一下第一種總線鎖的方式。通過在總線上聲言 LOCK# 信號,能夠有效的阻塞其他 CPU 對于總線的訪問,從而使得只能有一個 CPU 訪問變量所在的內存。在上面的 i = i + 1 代碼示例中,在代碼執(zhí)行的過程中,聲言了 LOCK# 信號后,那么只有等待 i = i + 1 的結果執(zhí)行完畢并應用到內存后,總線鎖才會解開,其他 CPU 才能夠繼續(xù)訪問內存中的變量,再繼續(xù)執(zhí)行后面的代碼,這樣就解決了緩存不一致問題。
但是上面的方式會有一個問題,由于在鎖住總線期間,其他 CPU 無法訪問內存,導致效率低下。
在 JDK 1.6 之后,優(yōu)化了 synchronized 聲言 LOCK# 的方式,不再對總線進行鎖定,轉而采取了對 CPU 緩存行進行鎖定,因為本篇文章不是介紹 synchronized 實現(xiàn)細節(jié)的文章,所以不再對這種方式進行詳細介紹,讀者只需要知道在優(yōu)化之后,synchronized 的性能不再成為并發(fā)問題的瓶頸了。
MESI 協(xié)議就是緩存一致性協(xié)議,即 Modified(被修改)Exclusive(獨占的) Shared(共享的) Or Invalid(無效的)。MESI 的基本思想就是如果發(fā)現(xiàn) CPU 操作的是共享變量,其他 CPU 中也會出現(xiàn)這個共享變量的副本,在 CPU 執(zhí)行代碼期間,會發(fā)出信號通知其他 CPU 自己正在修改共享變量,其他 CPU 收到通知后就會把自己的共享變量置為無效狀態(tài)。
并發(fā)編程中的三個主要問題
可見性問題
在單核 CPU 時代,所有的線程共用一個 CPU,CPU 緩存和內存的一致性問題容易解決,我們還拿上面的 i = 1 + 1 來舉例,CPU 和 內存之間如果用圖來表示的話我想會是下面這樣。
在多核時代,每個核都能夠獨立的運行一個線程,每個 CPU 都有自己的緩存,這時 CPU 緩存與內存的數(shù)據(jù)一致性就沒那么容易解決了,當多個線程在不同的 CPU 上執(zhí)行時,這些線程使用的是不同的 CPU 緩存。
因為 i 沒有經(jīng)過任何線程安全措施的保護,多個線程會并發(fā)修改 i 的值,所以我們認為 i 不是線程安全的,導致這種結果的出現(xiàn)是由于 aThread 和 bThread 中讀取的 i 值彼此不可見,所以這是由于 可見性 導致的線程安全問題。
原子性問題
當兩個線程開始運行后,每個線程都會把 i 的值讀入到 CPU 緩存中,再執(zhí)行 + 1 操作,然后把 + 1 之后的值寫入內存。因為線程間都有各自的虛擬機棧和程序計數(shù)器,他們彼此之間沒有數(shù)據(jù)交換,所以當 aThread 執(zhí)行 + 1 操作后,會把數(shù)據(jù)寫入到內存,同時 bThread 執(zhí)行 + 1 操作后,也會把數(shù)據(jù)寫入到內存,因為 CPU 時間片的執(zhí)行周期是不確定的,所以會出現(xiàn)當 aThread 還沒有把數(shù)據(jù)寫入內存時,bThread 就會讀取內存中的數(shù)據(jù),然后執(zhí)行 + 1操作,再寫回內存,從而覆蓋 i 的值。
有序性問題
在并發(fā)編程中還有帶來讓人非常頭疼的 有序性 問題,有序性顧名思義就是順序性,在計算機中指的就是指令的先后執(zhí)行順序。一個非常顯而易見的例子就是 JVM 中的類加載。
這是一個 JVM 加載類的過程圖,也稱為類的生命周期,類從加載到 JVM 到卸載一共會經(jīng)歷五個階段 加載、連接、初始化、使用、卸載。這五個過程的執(zhí)行順序是一定的,但是在連接階段,也會分為三個過程,即 驗證、準備、解析 階段,這三個階段的執(zhí)行順序不是確定的,通常交叉進行,在一個階段的執(zhí)行過程中會激活另一個階段。
在執(zhí)行程序的過程中,為了提高性能,編譯器和處理器通常會對指令進行重排序。重排序主要分為三類
編譯器優(yōu)化的重排序:編譯器在不改變單線程語義的情況下,會對執(zhí)行語句進行重新排序。
指令集重排序:現(xiàn)代操作系統(tǒng)中的處理器都是并行的,如果執(zhí)行語句之間不存在數(shù)據(jù)依賴性,處理器可以改變語句的執(zhí)行順序
內存重排序:由于處理器會使用讀/寫緩沖區(qū),出于性能的原因,內存會對讀/寫進行重排序
也就是說,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
volatile 的實現(xiàn)原理
上面聊了這么多,你可能都要忘了這篇文章的故事主角了吧?主角永遠存在于我們心中 …
其實上面聊的這些,都是在為 volatile 做鋪墊。
在并發(fā)編程中,最需要處理的就是線程之間的通信和線程間的同步問題,上面的可見性、原子性、有序性也是這兩個問題帶來的。
可見性
而 volatile 就是為了解決這些問題而存在的。Java 語言規(guī)范對 volatile 下了定義:Java 語言為了確保能夠安全的訪問共享變量,提供了 volatile 這個關鍵字,volatile 是一種輕量級同步機制,它并不會對共享變量進行加鎖,但在某些情況下要比加鎖更加方便,如果一個字段被聲明為 volatile,Java 線程內存模型能夠確保所有線程訪問這個變量的值都是一致的。
一旦共享變量被 volatile 修飾后,就具有了下面兩種含義
保證了這個字段的可見性,也就是說所有線程都能夠"看到"這個變量的值,如果某個 CPU 修改了這個變量的值之后,其他 CPU 也能夠獲得通知。
能夠禁止指令的重排序
下面我們來看一段代碼,這也是我們編寫并發(fā)代碼中經(jīng)常會使用到的
boolean isStop = false; while(!isStop){ ... } isStop = true;
1
2
3
4
5
6
在這段代碼中,如果線程一正在執(zhí)行 while 循環(huán),而線程二把 isStop 改為 true 之后,轉而去做其他事情,因為線程一并不知道線程二把 isStop 改為 true ,所以線程一就會一直運行下去。
如果 isStop 用 volatile 修飾之后,那么事情就會變的不一樣了。
使用 volatile 修飾了 isStop 之后,在線程二把 isStop 改為 true 之后,會強制將其寫入內存,并且會把線程一中 isStop 的值置為無效(這個值實際上是在緩存在 CPU 中的緩存行里),當線程一繼續(xù)執(zhí)行代碼的時候,會從內存中重新讀取 isStop 的值,此時 isStop 的值就是正確的內存地址的值。
volatile 有下面兩條實現(xiàn)原則,其實這兩條原則我們在上面介紹的時候已經(jīng)提過了,一種是總線鎖的方式,我們后面說總線鎖的方式開銷比較大,所以后面設計人員做了優(yōu)化,采用了鎖緩存的方式。另外一種是 MESI 協(xié)議的方式。
在 IA-32 架構軟件開發(fā)者的手冊中,有一種 Lock 前綴指令,這種指令能夠聲言 LOCK# 信號,在最近的處理器中,LOCK# 信號用于鎖緩存,等到指令執(zhí)行完畢后,會把緩存的內容寫回內存,這種操作一般又被稱為緩存鎖定。
當緩存寫回內存后,IA-32 和 IA-64 處理器會使用 MESI 協(xié)議控制內部緩存和其他處理器一致。IA-32 和 IA-64 處理器能夠嗅探其他處理器訪問系統(tǒng)內部緩存,當內存值修改后,處理器會從內存中重新讀取內存值進行新的緩存行填充。
由此可見,volatile 能夠保證線程的可見性。
那么 volatile 能夠保證原子性嗎?
原子性
我們還是以 i = i + 1 這個例子來說明一下,i = i + 1 分為三個操作
讀取 i 的值
自增 i 的值
把 i 的值寫會內存
我們知道,volatile 能夠保證修改 i 的值對其他線程可見,所以我們此時假設線程一執(zhí)行 i 的讀取操作,此時發(fā)生了線程切換,線程二讀取到最新 i 的值是 0 ,然后線程再次發(fā)生切換,線程一把 i 的值改為 1,線程再次切換,因為此時 i 的值還沒有應用到內存,所以線程 i 同樣把 i 的值改為 1 后,線程再次發(fā)生切換,線程一把 i 的值寫入內存后,再次發(fā)生切換,線程二再次把 i 的值寫會內存,所以此時,雖然內存值改了兩次,但是最后的結果卻不是 2。
那么 volatile 不能保證原子性,那么該如何保證原子性呢?
在 JDK 5 的 java.util.concurrent.atomic 包下提供了一些原子操作類,例如 AtomicInteger、AtomicLong、AtomicBoolean,這些操作是原子性操作。它們是利用 CAS 來實現(xiàn)原子性操作的(Compare And Swap),CAS實際上是利用處理器提供的 CMPXCHG 指令實現(xiàn)的,而處理器執(zhí)行 CMPXCHG 指令是一個原子性操作。
詳情可以參考筆者的這篇文章 一場 Atomic XXX 的魔幻之旅。
那么 volatile 能不能保證有序性呢?
這里就需要和你聊一聊 volatile 對有序性的影響了
###有序性
上面提到過,重排序分為編譯器重排序、處理器重排序和內存重排序。我們說的 volatile 會禁用指令重排序,實際上 volatile 禁用的是編譯器重排序和處理器重排序。
下面是 volatile 禁用重排序的規(guī)則
從這個表中可以看出來,讀寫操作有四種,即不加任何修飾的普通讀寫和使用 volatile 修飾的讀寫。
從這個表中,我們可以得出下面這些結論
只要第二個操作(這個操作就指的是代碼執(zhí)行指令)是 volatile 修飾的寫操作,那么無論第一個操作是什么,都不能被重排序。
當?shù)谝粋€操作是 volatile 讀時,不管第二個操作是什么,都不能進行重排序。
當?shù)谝粋€操作是 volatile 寫之后,第二個操作是 volatile 讀/寫都不能重排序。
為了實現(xiàn)這種有序性,編譯器會在生成字節(jié)碼中,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
這里我們先來了解一下內存屏障的概念。
內存屏障也叫做柵欄,它是一種底層原語。它使得 CPU 或編譯器在對內存進行操作的時候, 要嚴格按照一定的順序來執(zhí)行, 也就是說在 memory barrier 之前的指令和 memory barrier 之后的指令不會由于系統(tǒng)優(yōu)化等原因而導致亂序。
內存屏障提供了兩個功能。首先,它們通過確保從另一個 CPU 來看屏障的兩邊的所有指令都是正確的程序順序;其次它們可以實現(xiàn)內存數(shù)據(jù)可見性,確保內存數(shù)據(jù)會同步到 CPU 緩存子系統(tǒng)。
不同計算機體系結構下面的內存屏障也不一樣,通常需要認真研讀硬件手冊來確定,所以我們的主要研究對象是基于 x86 的內存屏障,通常情況下,硬件為我們提供了四種類型的內存屏障。
LoadLoad 屏障
它的執(zhí)行順序是 Load1 ; LoadLoad ;Load2 ,其中的 Load1 和 Load2 都是加載指令。LoadLoad 指令能夠確保執(zhí)行順序是在 Load1 之后,Load2 之前,LoadLoad 指令是一個比較有效的防止看到舊數(shù)據(jù)的指令。
StoreStore 屏障
它的執(zhí)行順序是 Store1 ;StoreStore ;Store2 ,和上面的 LoadLoad 屏障的執(zhí)行順序相似,它也能夠確保執(zhí)行順序是在 Store1 之后,Store2 之前。
LoadStore 屏障
它的執(zhí)行順序是 Load1 ; StoreLoad ; Store2 ,保證 Load1 的數(shù)據(jù)被加載在與這數(shù)據(jù)相關的 Store2 和之后的 store 指令之前。
StoreLoad 屏障
它的執(zhí)行順序是 Store1 ; StoreLoad ; Load2 ,保證 Store1 的數(shù)據(jù)被其他 CPU 看到,在數(shù)據(jù)被 Load2 和之后的 load 指令加載之前。也就是說,它有效的防止所有 barrier 之前的 stores 與所有 barrier 之后的 load 亂序。
JMM 采取了保守策略來實現(xiàn)內存屏障,JMM 使用的內存屏障如下
下面是一個使用內存屏障的示例
class MemoryBarrierTest { int a, b; volatile int v, u; void f() { int i, j; i = a; j = b; i = v; j = u; a = i; b = j; v = i; u = j; i = u; j = b; a = i; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
這段代碼雖然比較簡單,但是使用了不少變量,看起來有些亂,我們反編譯一下來分析一下內存屏障對這段代碼的影響。
從反編譯的代碼我們是看不到內存屏障的,因為內存屏障是一種硬件層面的指令,單憑字節(jié)碼是肯定無法看到的。雖然無法看到內存屏障的硬件指令,但是 JSR-133 為我們說明了哪些字節(jié)碼會出現(xiàn)內存屏障。
普通的讀類似 getfield 、getstatic 、 不加 volatile 修飾的數(shù)組 load 。
普通的寫類似 putfield 、 putstatic 、 不加 volatile 修飾的數(shù)組 store 。
volatile 讀是可以被多個線程訪問修飾的 getfield、 getstatic 字段。
volatile 寫是可以被當個線程訪問修飾的 putfield、 putstatic 字段。
這也就是說,只要是普通的讀寫加上了 volatile 關鍵字之后,就是 volatile 讀寫(呃呃呃,我好像說了一句廢話),并沒有其他特殊的 volatile 獨有的指令。
根據(jù)這段描述,我們來繼續(xù)分析一下上面的字節(jié)碼。
a、b 是全局變量,也就是實例變量,不加 volatile 修飾,u、v 是 volatile 修飾的全局變量;i、j 是局部變量。
首先 i = a、j = b 只是把全局變量的值賦給了局部變量,由于是獲取對象引用的操作,所以是字節(jié)碼指令是 getfield 。
從官方手冊就可以知曉原因了。
地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
由內存屏障的表格可知,第一個操作是普通讀寫的情況下,只有第二個操作是 volatile 寫才會設置內存屏障。
繼續(xù)向下分析,遇到了 i = v,這個是把 volatile 變量賦值給局部變量,是一種 volatile 讀,同樣的 j = u 也是一種 volatile 讀,所以這兩個操作之間會設置 LoadLoad 屏障。
下面遇到了 a = i ,這是為全局變量賦值操作,所以其對應的字節(jié)碼是 putfield
地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
所以在 j = u 和 a = i 之間會增加 LoadStore 屏障。然后 a = i 和 b = j 是兩個普通寫,所以這兩個操作之間不需要有內存屏障。
繼續(xù)往下面分析,第一個操作是 b = j ,第二個操作是 v = i 也就是 volatile 寫,所以需要有 StoreStore 屏障;同樣的,v = i 和 u = j 之間也需要有 StoreStore 屏障。
第一個操作是 u = j 和 第二個操作 i = u volatile 讀之間需要 StoreLoad 屏障。
最后一點需要注意下,因為最后兩個操作是普通讀和普通寫,所以最后需要插入兩個內存屏障,防止 volatile 讀和普通讀/寫重排序。
《Java 并發(fā)編程藝術》里面也提到了這個關鍵點。
從上面的分析可知,volatile 實現(xiàn)有序性是通過內存屏障來實現(xiàn)的。
關鍵概念
在 volatile 實現(xiàn)可見性和有序性的過程中,有一些關鍵概念,cxuan 這里重新給讀者朋友們嘮叨下。
緩沖行:英文概念是 cache line,它是緩存中可以分配的最小存儲單位。因為數(shù)據(jù)在內存中不是以獨立的項進行存儲的,而是以臨近 64 字節(jié)的方式進行存儲。
緩存行填充:cache line fill,當 CPU 把內存的數(shù)據(jù)載入緩存時,會把臨近的共 64 字節(jié)的數(shù)據(jù)一同放入同一個 Cache line,因為局部性原理:臨近的數(shù)據(jù)在將來被訪問的可能性大。
緩存命中:cache hit,當 CPU 從內存地址中提取數(shù)據(jù)進行緩存行填充時,發(fā)現(xiàn)提取的位置仍然是上次訪問的位置,此時 CPU 會選擇從緩存中讀取操作數(shù),而不是從內存中取。
寫命中:write hit ,當處理器打算將操作數(shù)寫回到內存時,首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器會將這個操作數(shù)寫回到緩存,而不是寫回到內存,這種方式被稱為寫命中。
內存屏障:memory barriers,是一組硬件指令,是 volatile 實現(xiàn)有序性的基礎。
原子操作:atomic operations,是一組不可中斷的一個或者一組操作。
如何正確的使用 volatile 變量
上面我們聊了這么多 volatile 的原理,下面我們就來談一談 volatile 的使用問題。
volatile 通常用來和 synchronized 鎖進行比較,雖然它和鎖都具有可見性,但是 volatile 不具有原子性,它不是真正意義上具有線程安全性的一種工具。
從程序代碼簡易性和可伸縮性角度來看,你可能更傾向于使用 volatile 而不是鎖,因為 volatile 寫起來更方便,并且 volatile 不會像鎖那樣造成線程阻塞,而且如果程序中的讀操作的使用遠遠大于寫操作的話,volatile 相對于鎖還更加具有性能優(yōu)勢。
很多并發(fā)專家都推薦遠離 volatile 變量,因為它們相對于鎖更加容易出錯,但是如果你謹慎的遵從一些模式,就能夠安全的使用 volatile 變量,這里有一個 volatile 使用原則
只有在狀態(tài)真正獨立于程序內其他內容時才能使用 volatile。
下面我們通過幾段代碼來感受一下這條規(guī)則的力量。
狀態(tài)標志
一種最簡單使用 volatile 的方式就是將 volatile 作為狀態(tài)標志來使用。
volatile boolean shutdownRequested; public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }
1
2
3
4
5
6
7
8
9
為了能夠正確的調用 shutdown() 方法,你需要確保 shutdownRequested 的可見性。這種狀態(tài)標志的一種特性就是通常只有一種狀態(tài)轉換:shutdownRequested 的標志從 false 轉為 true,然后程序停止。這種模式可以相互來回轉換。
雙重檢查鎖
使用 volatile 和 synchronized 可以滿足雙重檢查鎖的單例模式。
class Singleton{ private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) instance = new Singleton(); } } return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
這里說下為什么要用兩次檢查,假如有兩個線程,線程一在進入到 synchronized 同步代碼塊之后,在還沒有生成 Singleton 對象前發(fā)生線程切換,此時線程二判斷 instance == null 為 true,會發(fā)生線程切換,切換到線程一,然后退出同步代碼塊,線程切換,線程二進入同步代碼塊后,會再判斷一下 instance 的值,這就是雙重檢查鎖的必要所在。
讀-寫鎖
這也是 volatile 和 synchronized 一起使用的示例,用于實現(xiàn)開銷比較低的讀-寫鎖。
public class ReadWriteLockTest { private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } }
1
2
3
4
5
6
7
8
9
如果只使用 volatile 是不能安全實現(xiàn)計數(shù)器的,但是你能夠在讀操作中使用 volatile 保證可見性。如果你想要實現(xiàn)一種讀寫鎖的話,必須進行外部加鎖。
我自己肝了六本 PDF,全網(wǎng)傳播超過10w+ ,你需要關注一下我的 CSDN 賬號,私信回復 cxuan ,領取全部 PDF,這些 PDF 如下
下載鏈接 密碼:7im6
Java 任務調度
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。