Volatile關鍵字及其實現原理
在多線程並發編程中,Volatile可以理解為輕量級的Synchronized,用volatile關鍵字聲明的變量,叫做共享變量,其保證了變量的“可見性”以及“有序性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。可見性是由Java內存模型保證的(底層還是通過內存屏障實現的),即某個線程改變共享變量的值之后,會立即同步到主內存,線程每次使用共享變量的時候都先從內存中讀取刷新它的值;而有序性是通過“內存屏障”實現的,通過禁止指令重排序,從而使得某些代碼能以一定的順序執行。
但是Volatile關鍵字不能保證對共享變量操作的“原子性”,比如自增操作(i++)就不是原子操作,其可以分解為:①從內存中讀取i的值,②對i進行自增操作,③將i的值寫入到內存中;因此volatile關鍵字也不能完全保證多線程下的安全性。
在了解volatile關鍵字的原理之前,首先來看一下與其實現原理相關的CPU術語與說明。
圖1.與volatile實現原理相關的相關CPU術語與說明
通過對聲明了volatile變量的Java語句進行反編譯可以發現,有volatile變量修飾的共享變量進行寫操作的時候會多出第二行匯編代碼,其中Lock前綴的指令在多核處理器下會引發兩件事情:
- 將當前處理器緩存行的數據寫回到系統內存;
- 這個寫回內存的操作使得其他處理器緩存了該內存地址的數據無效。
這里的緩存指的是CPU的高速緩存,計算機為了提高處理的速度,處理器不直接和內存進行通信,而是將系統內存中的數據讀取到高速緩存(L1、L2等)之后再進行操作,但操作不知道何時會寫到內存。
如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作還是會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。
圖2.聲明了volatile變量的Java語句反編譯結果
Volatile內存語義的實現
為了實現Volatile的內存語義,JMM會分別限制編譯器重排序和處理器重排序。表3-5是JMM針對編譯器制定的volatile重排序規則表。
- 當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
- 當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。、
- 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
在編譯器生成字節碼的時候,會在指令序列中通過插入內存屏障來禁止特定類型的處理器重排序。JMM基於保守策略插入內存屏障。
- 在每個volatile寫操作的前面插入一個StoreStore屏障;
- 在每個volatile寫操作的后面插入一個StoreLoad屏障;
- 在每個volatile讀操作的后面插入一個LoadLoad屏障;
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
Synchronized及其實現原理
在多線程並發編程中synchronized一直是元老級角色,很多人稱其為重量級鎖,Java SE 1.6 對Synchronized進行了大量的優化,包括引入偏向鎖和輕量級鎖,減少了獲取鎖和釋放鎖帶來的性能消耗,使得Synchronized不再那么重量級。
Synchronized實現同步的基礎是:Java中每個變量都可以作為鎖. Synchronized在日常的使用中,主要有以下三種形式:
- 修飾普通方法,鎖的是當前實例變量(this);
- 修飾靜態(類)方法,鎖的是當前類的Class對象;
- 修飾同步方法快,鎖的是Synchronized括號中的對象。
當線程訪問同步方法或者代碼塊時,其必須先獲得鎖,在退出或者拋出異常時釋放鎖。JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的(隱式調用這兩個指令)。
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,兩兩配對。任何對象都有一個monitor與之關聯,當一個monitor被持有后,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。
Synchronized是可重入的,所謂可重入的意思就是,當某個線程獲得鎖時,再次請求進入有同一個鎖修飾的代碼塊或者方法時,操作會獲得成功。其中的其實原理課以簡單概括為moniter對象維護了一個線程持有者變量和一個鎖計數器,每當有線程嘗試獲取鎖的時候,如果當前鎖持有者為空,那么該線程將成功獲得鎖,同時計數器執行+1操作;如果當前鎖持有者不為空,那么會先檢查鎖的持有者跟當前請求鎖的線程是否是同一個,如果不是同一個,那么請求鎖失敗,該線程會進入阻塞狀態,如果當前請求獲取鎖的線程與鎖持有者是相同的,那么獲取鎖的請求會成功,且計數器會執行自增操作,沒退出一個同步方法或者執行代碼塊,計數器會-1,直到為0,此時鎖處於空閑狀態。
Java對象頭
Synchronized所用的鎖是存在Java對象頭里面的,如果對象是數組類型,虛擬機會用3個字寬(Word)來存儲對象頭,否則用2個字寬來存儲對象頭。在32位虛擬機中,1字寬=4字節=32bit.
圖3.Java對象頭的長度
Java對象頭里的Mark Word里默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如下圖所示:
圖4.Java對象頭存儲結構
在運行期間Mark Word里存儲的數據會隨着鎖標志位的變化而變化:
鎖的升級與對比
在Java SE 1.6中鎖一共有四種狀態,從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
1.偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活着,如果線程不處於活動狀態,則將對象頭設置成無鎖狀態;如果線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向於其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。圖2-1中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。
2. 輕量級鎖
(1)輕量級鎖加鎖
線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
(2)輕量級鎖解鎖
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。圖2-2是兩個線程同時爭奪鎖,導致鎖膨脹的流程圖。
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
Happens-before簡介
從JDK 5開始,Java使用新的JSR-133內存模型(除非特別說明,本文針對的都是JSR-133內存模型)。JSR-133使用happens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。與程序員密切相關的happens-before規則如下。
- 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意后續操作。
- 監視器鎖規則:對一個鎖的解鎖,happens-before於隨后對這個鎖的加鎖。
- volatile變量規則:對一個volatile域的寫,happens-before於任意后續對這個volatile域的讀。
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
兩個操作之間具有happens-before關系,並不意味着前一個操作必須要在后一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。
as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序.
Java多線程
線程:
線程是系統調度的基本單位。每個線程都有自己的程序計數器、Java虛擬機棧、本地方法棧。
等待/通知的相關方法
等待/通知的相關方法是任意Java對象都具備的,因為這些方法被定義在所有對象的超類java.lang.Object上,方法和描述如表4-2所示。
Objecte類擁有的方法有:hashcode、getClass、wait、notify、notifyAll、equals、clone、finalize、toString方法。
調用wait()、notify()以及notifyAll()時需要注意的細節,如下:
- 使用wait()、notify()和notifyAll()時需要先對調用對象加鎖。
- 調用wait()方法后,線程狀態由RUNNING變為WAITING,釋放所占用的對象鎖,並將當前線程放置到對象的等待隊列。
- notify()或notifyAll()方法調用后,等待線程依舊不會從wait()返回,需要調用notify()或notifAll()的線程釋放鎖之后,等待線程才有機會從wait()返回。
- notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING 變為BLOCKED。
- 從wait()方法返回的前提是獲得了調用對象的鎖。
在圖4-3中,WaitThread首先獲取了對象的鎖,然后調用對象的wait()方法,從而放棄了鎖並進入了對象的等待隊列WaitQueue中,進入等待狀態。由於WaitThread釋放了對象的鎖,NotifyThread隨后獲取了對象的鎖,並調用對象的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變為阻塞狀態。NotifyThread釋放了鎖之后,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。
Lock接口
Java SE 5之后,並發包Java.util.concurrent引入了Lock接口來實現鎖功能,它與synchronized的同步功能相類似,但是在使用時需要顯式地獲取鎖和釋放鎖(在finally塊中釋放鎖,目的是保證在獲取到鎖之后,最終能夠被釋放),其次Lock還擁有可中斷地獲取鎖以及超時獲取鎖等Synchronized不具備的功能。
Lock是一個接口,它定義了鎖獲取和釋放的基本操作,Lock的API如表5-2所示。
加鎖調用鏈為Lock.lock()->Lock.acquire()->Sync.tryAcquire();解鎖調用鏈為Lock.unlock()->Lock.release()->Sync.tryRelease();其中Sync是Lock的一個靜態內部類,Sync繼承自AQS,.acquire()和.release()是AQS的模板方法,最終調用由Sync重寫的tryAcquire()和tryRelease()方法;
隊列同步器AQS(Abstract Queued Synchronizer)
隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因為它們能夠保證狀態的改變是安全的。同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨占式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
顧名思義,獨占鎖就是在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖。
實現自定義同步組件時,將會調用同步器提供的模板方法,這些(部分)模板方法與描述如表5-4所示。
隊列同步器AQS的實現分析
AQS主要包括:同步隊列、獨占式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等同步器的核心數據結構與模板方法。
- 同步隊列
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成為一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
獨占式同步狀態獲取流程,也就是acquire(int arg)方法調用流程,如圖5-5所示。
分析了獨占式同步狀態獲取和釋放過程后,適當做個總結:在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然后喚醒頭節點的后繼節點。
獨占式超時獲取同步狀態doAcquireNanos(int arg,long nanosTimeout)和獨占式獲取同步狀態acquire(int args)在流程上非常相似,其主要區別在於未獲取到同步狀態時的處理邏輯。acquire(int args)在未獲取到同步狀態時,將會使當前線程一直處於等待狀態,而doAcquireNanos(int arg,long nanosTimeout)會使當前線程等待nanosTimeout納秒,如果當前線程在nanosTimeout納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。
共享式同步狀態獲取與釋放
共享式獲取與獨占式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。
LockSupport工具
當需要阻塞或喚醒一個線程的時候,都會使用LockSupport工具類來完成相應工作。LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成為構建同步組件的基礎工具。
LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程。
Condition接口
任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的。
阻塞隊列
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作支持阻塞的插入和移除方法。
- 支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
- 支持阻塞的移除方法:意思是在隊列為空時,獲取元素的線程會等待隊列變為非空。
阻塞隊列常用於生產者和消費者的場景,生產者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。
Java里面的7個阻塞隊列
JDK 7提供了7個阻塞隊列,如下。
- ·ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列,默認非公平,FIFO。
- ·LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列,默認和最大長度為
- Integer.MAX_VALUE , FIFO。
- ·PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
- ·DelayQueue:一個使用優先級隊列實現的無界阻塞隊列,支持延時獲取元素。
- ·SynchronousQueue:一個不存儲元素的阻塞隊列,默認非公平。
- ·LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
- ·LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
Java中的13個原子操作類
Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。
因為變量的類型有很多種,所以在Atomic包里一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本類型、原子更新數組、原子更新引用和原子更新屬性(字段)。Atomic包里的類基本都是使用Unsafe實現的包裝類。
ActomicInteger/AtomicBoolean/AtomicLong/AtomicReference
getAndIncreasement()/getAndSet()/compareAndSet)方法
CountDownLatch/CyclicBarrier/Semaphore
用法:CountDownLatch c=new CountDownLatch(2);
CountDownLatch內部有個靜態內部類繼承自AQS,每個線程執行到某個位置后,執行countdownlatch.countDown()方法使得數字減一,減到0為止;所有線程都會阻塞在c.await()直到c.countDown()減到0為止,然后繼續執行;
用法:CyclicBarrier c = new CyclicBarrier(2);
CyclicBarrier中,每個線程執行到某個位置時,調用c.awit()通知主線程或者某個特定線程表示自己已經到達循環屏障了,當達到循環屏障的線程數量等於構造函數中的數字時;所有線程都再繼續往下執行;
CyclicBarrier相較於CountDownLatch來說可以實現一些更高級的功能,比如可以重置計數器,比如可以知道阻塞的線程數,比如可以知道哪些線程被中斷,比如可以加入別的任務,在所有線程都到達屏障時優先執行該任務。
Semaphore
Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。多年以來,我都覺得從字面上很難理解Semaphore所表達的含義,只能把它比作是控制流量的紅綠燈。比如××馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,后面的車會看到紅燈,不能駛入××馬路,但是如果前一百輛中有5輛車已經離開了××馬路,那么后面就允許有5輛車駛入馬路,這個例子里說的車就是線程,駛入馬路就表示線程在執行,離開馬路就表示線程執行完成,看見紅燈就表示線程被阻塞,不能執行。
Semaphore可以用於做流量控制,特別是公用資源有限的應用場景,比如數據庫連接。假如有一個需求,要讀取幾萬個文件的數據,因為都是IO密集型任務,我們可以啟動幾十個線程並發地讀取,但是如果讀到內存后,還需要存儲到數據庫中,而數據庫的連接數只有10個,這時我們必須控制只有10個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。這個時候,就可以使用Semaphore來做流量控制,如代碼清單8-7所示。
Java中的線程池
使用線程池的好處:
- 降低資源消耗;通過重復利用已經創建的線程降低線程創建和銷毀造成的消耗。
- 提高響應速度;當任務達到時,任務可以不需要等到線程創建就能立即執行。
- 提高線程的可管理性;線程不能無限制地創建,否則會消耗系統資源以及降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。
利用Executors.newFixedThreadPool()創建一個含有N個線程的線程池;其中工廠方法調用ThreadPoolExecutor的構造方法,創建了一個核心線程數和最大線程數都是N的線程池,該線程池的keepalivedTime設置為0L,意味着,非核心線程一旦閑暇就會被終止,同時其采用LinkedBlockingQueue(一種以鏈表為基礎的游街阻塞隊列,不傳參數默認創建Integer。MAX_VALUE大小的阻塞隊列)
1 //利用Executors.newFixedThreadPool
2
3 public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { 4 return new ThreadPoolExecutor(nThreads, nThreads, 5 0L, TimeUnit.MILLISECONDS, 6 new LinkedBlockingQueue<Runnable>(), 7 threadFactory); 8 }
利用Executors.newSingleThreadExecutor()創建一個大小為1的線程池;其最終調用的是ThreadPoolExecutor的構造方法創建了一個核心線程數和最大線程數都為1的線程池,keepaliveTime設置為0L,意味着非核心線程一旦閑下來就會被終止,其采用LinkedBlockingQueue作為阻塞隊列。
1 public static ExecutorService newSingleThreadExecutor() { 2 return new FinalizableDelegatedExecutorService 3 (new ThreadPoolExecutor(1, 1, 4 0L, TimeUnit.MILLISECONDS, 5 new LinkedBlockingQueue<Runnable>())); 6 }
利用Executors.newCachedThreadPool()創建一個核心線程數為0,最大線程數為Integer.MAX_VALUE,keepAlivedTime為60s,並且使用SynchronousQueue阻塞隊列來做任務隊列().CachedThreadPool使用沒有容量的SynchronousQueue作為線程池的工作隊列,但CachedThreadPool的maximumPool是無界的。這意味着,如果主線程提交任務的速度高於maximumPool中線程處理任務的速度時,CachedThreadPool會不斷創建新線程。極端情況下,CachedThreadPool會因為創建過多線程而耗盡CPU和內存資源。
1 public static ExecutorService newCachedThreadPool() { 2 return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 3 60L, TimeUnit.SECONDS, 4 new SynchronousQueue<Runnable>()); 5 }