摘要:限期阻塞調用方法等待時間結束或線程執行完畢。終止狀態線程執行完畢或出現異常退了。和都會檢查線程何時中斷,并且在發現中斷時提前放回。工廠方法將線程池的最大大小設置為,而將基本大小設置為,并將超時大小設置為分鐘。
wait()、notify()、notifyAll()
Object是所有類的基類,它有5個方法組成了等待、通知機制的核心:notify()、notifyAll()、wait()、wait(long) 和wait(long,int)。在java中,所有的類都是從Object繼承而來,因此,所有的類都擁有這些共同的方法可供使用。
wait()public final void wait() throws InterruptedException,IllegalMonitorStateException
該方法用來將當前線程置入休眠狀態,直到接到通知或中斷為止。在調用wait()之前,線程必須要獲得對象的對象級別的鎖,即只能在同步方法或同步代碼塊中調用wait()方法。進入wait()方法后,當前線程釋放鎖。在從wait()返回前,線程與其他線程競爭重新獲得鎖。如果調用wait()時,沒有持有適當的鎖,則拋出IllegalMonitorStateException,它是RuntimeException的一個子類,因此不需要try-catch結構。
notify()public final native void notify() throws IllegalMonitorStateException
該方法也要在同步方法或同步代碼塊中調用,即在調用前,線程也必須要獲得該對象的對象級別鎖,如果調用notify()時沒有持有適當的鎖,也會拋出IllegalMonitorStateException。
該方法用來通知那些等待該對象的對象鎖的其他線程。如果有多個線程等待,則線程規劃器任意挑選其中一個wait()狀態得線程來發出通知,并使它們等待獲取該對象的對象鎖。(notify后,當前線程不會馬上釋放對象鎖,wait所在的線程并不能馬上獲取該對象鎖,要等到程序退出synchronized代碼塊后,當前線程才會釋放鎖,wait所在的線程才可以獲得該對象鎖)。
但不驚動其他同樣等待該對象notify的線程們,當第一個獲得了該對象的wait線程運行完畢之后,它會釋放該對象的鎖,此時如果該對象沒有再次使用notify語句,則即便對象已經空閑,其他wait狀態等待的線程由于沒有得到該對象的通知,會繼續阻塞在wait狀態,直到這個對象發出一個notify或notifyAll。
wait狀態等待的是被notify而不是鎖。
notifyAll()public final native void notifyAll() throws IllegalMonitorStateException
該方法與notify()方法的工作方式相同,重要的一點差異:
notifyAll使所有的方法在該對象wait的線程統統退出wait狀態,變成等待獲取該對象上的鎖,一旦該對象鎖被釋放(即notifyAll線程退出同步代碼塊時),它們就會去競爭。如果其中一個線程獲得了該對象的鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖后,其他的已經被喚醒的線程將會繼續競爭該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。
public class WaitAndNotify { public static void main(String[] args) throws InterruptedException{ WaitAndNotify wan = new WaitAndNotify(); // synchronized(wan){ // wan.wait(); // System.out.println("wait"); // } new Thread(new Runnable(){ public void run(){ synchronized(wan){ try { wan.wait(); System.out.println("wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable(){ public void run(){ synchronized(wan){ try { wan.wait(); System.out.println("wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable(){ public void run(){ synchronized(wan){ wan.notifyAll(); //當notify方法時只執行一個wait、而notifyAll方法將執行兩個wait System.out.println("notify"); } } }).start(); } }線程的狀態及其轉換 新建
創建之后未啟動
就緒狀態調用了start方法,不過還未被OS調用,或者正在等待CPU時間片。
運行狀態正在被OS執行。
阻塞狀態阻塞狀態分三種:
同步阻塞:線程正在等待一個排它鎖。
限期阻塞:調用Thread.sleep()、join()方法等待sleep時間結束或join線程執行完畢。
無限期阻塞:通過wait()方法進入,等待notify()或notifyAll()方法喚醒。
終止狀態線程執行完畢或出現異常退了run()。
volatile 作用volatile可以保證線程的可見性并提供一定的有序性,但是無法保證原子性。
原理在JVM底層,volatile是采用內存屏障來實現的。
內存屏障內存屏障是一個cpu指令,他的作用:
確保一些特定操作執行的順序
影響一些數據的可見性(可能是某些指令執行后的結果)。編譯器和cpu在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。插入一個內存屏障,相當于告訴CPU和編譯器先于這個命令的必須先執行,后于這個命令的必須后執行。
內存屏障跟volatile的關系如果字段是volatile,java內存模型將在寫操作后插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。這就意味著如果你對一個volatile字段進行寫操作,那么,一旦你完成寫入,任何訪問這個字段的線程都將會得到最新的值;在你寫入之前,會保證之前發生的事情已經發生,并且任何更新過的數據值都是可見的。
任務執行串行執行任務:在單個線程中串行地執行各項任務。
顯式地為任務創建線程:通過為每個線程創建一個新的線程來提供服務,從而實現更高的響應性。這種方式存在一定缺陷:
線程生命周期的開銷非常高:線程的創建于銷毀需要消耗計算資源。
資源消耗:活躍的線程會消耗系統資源,尤其是內存。如果在可用處理器的數量小于可運行的線程數量時,那么有些線程將會被閑置。
穩定性:在可創建線程的數量上存在一個限制,如果破壞了這些限制,那么可能會拋出OutOfMemoryError異常。
線程池
Executor框架:Executor雖然是一個簡單的接口,但它卻為靈活且強大的異步任務執行框架提供了基礎。它提供了一種標準的方法將任務的提交過程與執行過程解耦開來,應用Runnable來表示任務。同時它還提供了對生命周期的支持。基于生產者-消費者模式,提交任務的操作相當于生產者,執行任務的線程則相當于消費者。
Executor的生命周期Executor的實現通常會創建線程來執行任務,但如果無法正確關閉Executor,那么jvm也將無法關閉。
為了解決執行服務的生命周期問題,Executor擴展了ExecutorSerivce借口,添加了一些用戶生命周期管理的方法。
ExecutorService的生命周期有三種狀態:運行、關閉和已終止。
ExecutorService在初始化創建時為運行狀態。
Shutdown方法將執行平緩的關閉過程:不在接受新的任務,同時等待已經提交的任務執行完成——包括還未開始執行的任務。
ShutdownNow方法將執行粗暴的關閉過程:它將嘗試取消所有運行的任務,并且不在啟動隊列中尚未開始執行的任務。
Callable跟FutureRunnable是一定有很大局限的抽象,它不能返回一個值或拋出一個受檢查的異常。
Callable是一種更好的抽象,它認為主入口點(cell)能夠返回一個值,并可能拋出一個異常。
Future表示一個任務的生命周期,并提供相應的方法來判斷是否完成或取消。Executor執行的任務有4個生命周期:創建、提交、開始和完成。任務的生命周期只能前進不能后退。Future的get方法的行為取決于任務的狀態,如果完成,那么get會立即返回或者拋出一個Exception,如果任務沒有完成,那么get將阻塞并指導任務完成,如果任務拋出異常,那么get將該異常封裝為ExecutionException并重新拋出。
ExecutorService的所有submit方法都返回一個Future,從而將一個Runnable或Callable提交給Executor,并得到一個Futrue用來獲取任務的執行結果或者取消任務。
顯式的為某個指定的Runnable或Callable實例化一個FutureTask。(FutureTask實現了Runnable,因此可以將它交給Executor來執行或者直接調用它的run方法)
線程中斷:Java并沒有提供任何機制來安全的終止線程,但它提供了中斷,這是一種協作機制,能夠使一個線程終止另一個線程的當前工作。
Thread的中斷方法:
public class Thread{ public void interrupt(){….} public Boolean isInterrupted(){….} public static Boolean interrupted(){….} }
對中斷的正確理解:他并不會真正的中斷一個正在運行的線程,而只是發出中斷請求,然后由線程在下一個合適的時刻中斷自己。
Interrupt():中斷目標線程,
IsInterrupt():放回目標線程的中斷狀態
Interrupted():清除當前線程的中斷狀態,并返回它之前的值,這也是清除中斷狀態的唯一方法。
Thread.sleep()和Object.wait()都會檢查線程何時中斷,并且在發現中斷時提前放回。它們在響應中斷時執行的操作:清除中斷狀態,拋出InterruptedException,表示阻塞操作由于中斷而提前結束。能夠中斷處于阻塞、限期等待、無限期等待等狀態,但不能中斷I/O阻塞跟synchronized鎖阻塞。
在調用interrupted()時返回true,除非像屏蔽這個中斷,不然就必須對它進行處理。如果一個線程的 run() 方法執行一個無限循環,并且沒有執行 sleep() 等會拋出 InterruptedException 的操作,那么調用線程的 interrupt() 方法就無法使線程提前結束。
但是調用 interrupt() 方法會設置線程的中斷標記,此時調用 interrupted() 方法會返回 true。因此可以在循環體中使用 interrupted() 方法來判斷線程是否處于中斷狀態,從而提前結束線程。
shutdownNow跟shutdown
Future擁有一個cancel方法,該方法帶有一個boolean類型參數mayInterruptIfRunning,表示取消操作是否成功,如果mayIterruptIfRunning為true并且任務當前正在某個線程中執行,那么這個線程能被中斷,如果mayInterruptIfRunning為false,那么意味著,若任務還沒有啟動,就不要執行它。
線程池線程池:管理一組同構工作線程的資源池,通過重用現有的線程而不是創建新線程,可以在處理多個請求時分攤在線程創建和銷毀過程中產生巨大開銷。
類庫提供了一個靈活的線程池以及一些有用的默認配置。可以用過調用Executors中的靜態工廠方法之一來創建一個線程池:
newFixedThreadPool: 創建一個固定長度的線程池,每當提交一個任務時就創建一個線程,直到達到線程池的最大數量。newFixedThreadPool工廠方法將線程池的基本大小和最大大小設置為參數中的值,并且創建的線程池不會超時。
newCachedThreadPool: 創建一個可緩存的線程池,如果線程池的當前規模超過了處理需求時,那么將回收空閑的線程,而當需求添加時,則可以添加新的線程,線程池的規模不存在任何限制。newCacheThreadPool工廠方法將線程池的最大大小設置為Integer.MAX_VALUE,而將基本大小設置為0,并將超時大小設置為1分鐘。
newSingleThreadPool: 一個單線程Executor,創建單個工作線程來執行任務,如果這個線程異常結束,會創建另一個線程來替代。newSingleThreadPool能夠確保依照任務在隊列中的順序來串行執行(例如FIFO、LIFO、優先級)
ThreadPoolExecutorThreadPoolExecutor為一些Executor提供了基本實現,這些Executor時由Executors中的newFixedThreadPool、newCachedThreadPool和newScheduledThreadExecutor等工廠方法返回的。
ThreadPoolExecutor是一個靈活的、穩定的線程池,允許進行各種定制。如果默認的執行策略不能滿足需求,那么可以通過ThreadPoolExecutor的構造參數來實例化一個對象,并根據自己的需求來定制。
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler){…}
線程池的基本大小,最大大小以及存活時間等因素公共負責線程的創建和銷毀。
基本大小(corePoolSize):線程池的目標大小,即在沒有任務執行時線程池的大小,并且只有 在工作隊列滿了的情況下才會創建超過這個數量的線程。
最大大小(maximumPoolSize):表示可同時活動的線程數量的上限。
存活時間(keepAliveTime):當某個線程的空閑時間超過了存活時間,那么將被標記為可回收的,并且當線程的當前大小超過了基本大小時,這個線程將被終止。
管理隊列任務在線程池中,如果新請求的到達速率超過線程池的處理速率,那么新到來的請求將積累起來。在線程池中,這些請求會在一個由Executor管理的Runnable隊列中等待。
ThreadPoolExecutor允許提供一個BlockingQueue來保存等待執行的任務。基本的任務排隊方法有三種:有界隊列、無界隊列和同步移交。隊列的選擇與其他的配置參數有關,例如線程池的大小等。
newFixedThreadPool和newSingleThreadPool在默認情況下使用一個無界的LinkedBlockingQueue。
一個更穩妥的資源管理策略是使用有界隊列,例如ArrayBlockingQueue、有界的LinkedBlockingQueu、PriorityBlockingQueue。通過飽和策略來解決隊列填滿之后,新任務到來的情況。
對于非常大的或者無界的線程池,可以通過使用SynchronousQueue來避免任務排隊,以及直接將任務從生產者持戒交給工作者線程。SynchronousQueue并不是一個真正的隊列,而是一種在線程之間隊形移交的機制。要將線程一個元素放入SynchronousQueue中,必須有另一個線程在等待接受。如果沒有線程等待,并且線程池的當前大小小于最大值,那么ThreadPoolExecutor將創建一個新的線程。否則根據飽和策略,這個任務將被拒絕。使用直接移交更高效。只有線程池時無界或者可以拒絕任務時,SynchronousQueue才有實際價值。在newCacheThreadPool工廠方法中就使用了SynchronousQueue。
飽和策略當有界隊列被填滿之后,飽和策略開始發揮作用。JDK提供了集中不同的RejectedExecutionHadler來實現。
終止(AbortPolicy):默認的飽和策略,將策略將拋出未檢查的RejectExecutionException,調用者可以捕獲這個異常進行處理。
拋棄(DiscardPolicy):將悄悄拋棄該任務。
拋棄最舊的(DiscardOldestPolicy):拋棄下一個將被執行的任務,然后嘗試重新提交新的任務。(如果工作隊列是一個優先隊列,那么將拋棄優先級最高的任務)
調用者執行(CallerRunsPolicy):將任務會退給調用者,從而降低任務的流量。它不是在線程池中的線程執行新提交的任務,而是在一個調用了execute的線程執行該任務。在WebServer中使用有界隊列且”調用者運行”飽和策略時,并且線程池所有的線程都被占用,隊列已滿的情況下,下一個任務將會由調用execute的主線程執行,此時主線程在執行任務期間不會accept請求,故新到來的請求被保存在TCP層的隊列中而不是應用程序的隊列中,如果持續過載,那么TCP層的請求隊列最終將被填滿,因為同樣會開始拋出請求。
線程工廠:每當線程池需要創建一個線程時,都是通過線程工廠方法來完成的,默認的線程工廠方法將創建一個新的、非守護的線程,并且不包含特殊的配置信息。通過制定一個特定的工廠方法,可以定制線程池的配置信息。在ThreadFactory中只定義了一個方法newThread,每當線程池需要創建一個新線程時都會調用這個方法。
public interface ThreadFactory { /** * Constructs a new {@code Thread}. Implementations may also initialize * priority, name, daemon status, {@code ThreadGroup}, etc. * * @param r a runnable to be executed by new thread instance * @return constructed thread, or {@code null} if the request to * create a thread is rejected */ Thread newThread(Runnable r); }顯式鎖
Java5.0增加了一種新的機制:ReentranLock。與內置加鎖機制不同的時,Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的。
Lock lock = new ReentrantLock(); //... lock.lock(); try { //更新對象狀態 //捕獲異常,并在必要時恢復不變形條件 } finally { lock.unlock(); //不會自動清除鎖 }
當程序的執行控制單元離開被保護的代碼塊時,并不會自動清除鎖。
輪詢鎖與定時鎖可定時的與可輪詢的鎖獲取模式是由tryLock方法實現的,與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制。在內置鎖中,死鎖是一個很嚴重的問題,恢復程序的唯一方式是重新啟動程序,而防止死鎖的唯一方法就是在構造程序時避免出現不一致的鎖順序。可定時的與可輪詢的鎖提供了另一種選擇:避免死鎖的發生。
如果不能獲得所有需要的鎖,那么可以使用可定時的或可輪詢的鎖獲取方式,從而使你重新獲得控制權,它會釋放已經獲得的鎖,然后重新嘗試獲取所有鎖。(或其他操作)
在實現具有時間限制的操作時,定時鎖同樣費用有用:當在帶有時間限制的操作中調用一個阻塞方法時,它能根據剩余時間來提供一個時限,如果不能在制定的時間內給出結果,那么就會使程序提前結束。
可中斷鎖內置鎖是不可中斷的,故有時將使得實現可取消得任務變得復雜,而顯示鎖可以中斷,lockInterruptibly方法能夠獲得鎖的同時保持對中斷的響應。LockInterruptibly優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取,既允許線程在還等待時就中斷。(lock優先考慮獲取鎖,待獲取鎖成功之后才響應中斷)
公平性在ReentrantLock的構造函數中提供了兩種公平性選擇:創建一個非公平的鎖(默認)或者一個公平的鎖,在公平的鎖上,線程講按照它們發出的請求的順序來獲取鎖,但在非公平鎖上,則允許插隊(當一個線程請求非公平鎖時,同時該鎖的狀態變為可用,那么這個線程將跳過隊列中所有的等待線程并獲得這個鎖)。
在公平鎖中,如果有一個線程持有這個鎖或者有其他線程在隊列中等待這個鎖,那么新發的請求的線程將會放入隊列中,而在非公平鎖中,只有當鎖被某個線程持有時,新發出的請求的線程才會放入隊列中。
大多數情況下,非公平鎖的性能要高于公平鎖
公平性鎖在線程的掛起以及恢復中需要一定開銷
假設線程A持有一個鎖,并且線程B請求這個鎖,那么在A釋放時,講喚醒B,這時C也請求這個鎖,那么可能C很可能會在B被完全喚醒之前就執行完了。
java內存模型Java虛擬機規范試圖定義一種java內存模型(Java Memory Model,JMM)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
主內存和工作內存計算機系統中處理器上的寄存器的讀寫速度比內存要快幾個數量級別,為了解決這種矛盾,在它們之間加入高速緩存。
加入高速緩存的存儲交互很好的解決了處理器與內存的速度矛盾,但是也為計算機系統帶來了更高的復雜度,因為它引入了一個新的問題:緩存一致性。
多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存,當多個處理器的運算任務涉及到同一塊主內存區域時,將可能導致各自的緩存不一致。
為了解決一致性問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時根據協議來進行操作。
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存以及從內存中取出變量的底層細節。
JMM將所有變量(實例字段、靜態字段、數組對象的元素,線程共享的)都存儲到主內存中,每個線程有自己的工作內存,工作內存中保存了被線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行。
內存間的交互操作Java內存模型定義了8個操作來完成主內存和工作內存間的交互操作。
read:把一個變量從主內存傳輸到工作內存。
load:在read之后執行,把read得到的值放入工作內存的變量副本中。
use:把工作內存中的一個變量的值傳遞給執行引擎。
assign:把一個從執行引擎接收到的值賦給工作內存的變量。
store:把工作內存的一個變量傳送到主內存中。
write:在store之后執行,把store得到的值放入主內存的變量中。
lock:作用與主內存變量,它把一個變量標識為一條線程獨占的狀態。
unlock:把處于鎖定狀態得變量釋放出來。
內存模型的三大特性 原子性Java內存模型保證了read、load、use、assign、store、write、lock和unlock操作具有原子性,例如對一個int類型的變量執行assign復制操作,這個操作就是原子性的。但是Java內存模型允許虛擬機將沒有被volatile修飾的64位數據(long,double)的讀寫操作劃分為兩個32位的操作來進行,即load、store、read和write操作可以不具備原子性。
雖然Java內存模型保證了這些操作的原子性,但是int等原子性操作在多線程中還是會出現線程安全性問題。
可通過兩個方式來解決:
使用AtomicInteger
通過synchronized互斥鎖來保證操作的原子性。
可見性可見性指一個線程修改共享變量的值,其他線程能夠立即得知這個修改。Java內存模型通過在變量修改后將新值同步回主內存中,在變量讀取前從主內存刷新變量值來實現可見性。
主要有三種方式實現可見性:
volatile
synchronized,對一個變量執行unlock操作之前,必須把變量值同步回主內存。
final:被final修飾的字段在構造器中一旦初始化完成,并且沒有發生this逃逸(其他線程有可能通過這個引用訪問到初始化了一半的對象),那么其他線程就能夠看見final字段的值。
有序性有序性是指:在本線程內觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因為發生了指令重排序。在Java內存模型中,允許編譯器和處理器對指令進行重排序,重排序過程中不會影響到單線程程序的執行,卻會影響多線程并發執行的正確性。
volatile關鍵字通過添加內存屏障的方式禁止重排序,即重排序時不能把后面的指令放在內存屏障之前。
也可以通過synchronized來保證有序性,它保證每個時刻只有一個線程執行同步代碼,相當于讓線程順序執行同步代碼。
線程安全的方法 互斥同步synchronized和ReentranLock。、
非阻塞同步互斥同步最主要的問題是線程阻塞和喚醒所帶來的性能問題,因此這種這種同步燁稱為阻塞同步。
互斥同步是一種悲觀的并發策略,總是以為只要不去做正確的同步策略,那就肯定會出問題,無論共享數據是否真的會出現競爭,它都需要進行枷鎖。
CAS隨著硬件指令集的發展,我們可以使用基于沖突檢測的樂觀并發策略:先進行操作,如果沒有其他線程競爭共享資源,那么操作就成功了,否則采取補償措施(不斷嘗試、知道成功為止)。這種樂觀的并發策略的許多實現都不需要將線程阻塞,因此這種同步操作稱為非阻塞同步。
樂觀鎖需要操作和沖突檢測這兩個步驟具備原子性,這里就不能再使用互斥同步來保證,只能靠硬件來完成。硬件支持的原子性操作最典型的是:比較和交換(Compare-And-swap,CAS)。CAS指令需要3個操作數,分別是內存地址V、舊的預期值A和新值B。當操作完成時,只有內存地址V等值舊的預期值時,才將V值更新為B。
無同步方案 棧封閉多個線程訪問同一個方法的局部變量時,不會出現線程安全問題,因為局部變量存儲在虛擬機棧中,屬于線程私用。
本地線程存儲(Thead Local Storage)如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。如果能保證,我們就可以把共享數據的可見范圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。
符合這種特點的應用并不少見,大部分使用消費隊列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程盡量在一個線程中消費完。其中最重要的一個應用實例就是經典 Web 交互模型中的“一個請求對應一個服務器線程”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多 Web 服務端應用都可以使用線程本地存儲來解決線程安全問題。
可以使用 java.lang.ThreadLocal 類來實現線程本地存儲功能。
synchronized的實現synchronized關鍵字在經過編譯之后,會在同步代碼塊前后分別形成monitorenter和monitorexit兩個字節碼指令。
在執行monitorenter指令時,首先嘗試獲取對象的鎖,如果這個對象沒有被鎖定或者當前線程已經擁有了這個鎖,就把鎖的計算器+1,相應的執行完monitorexit指令時將鎖計算器減1,當計算器為0時,鎖就被釋放。
鎖優化指JVM對synchronized的優化。
自旋鎖互斥同步進入阻塞狀態的開銷都很大,應該盡量避免,在許多應用中,共享數據的鎖定狀態只會持續很短的一段時間。自旋鎖的思想是讓一個線程在共享數據的鎖執行忙循環(自旋)一段時間,如果在這段時間內能夠獲得鎖,就可以避免進入阻塞狀態。
自旋鎖雖然能夠避免進入阻塞狀態從而減少開銷,但是它需要進行忙循環操作占用cpu時間,它只適用于共享數據的鎖定狀態很短的場景。
在JDK1.6中引入了自適應的自旋鎖,自適應意味著自旋次數不再是固定的,而是由前一次在同一個鎖上的自旋次數及鎖的擁塞者的狀態來決定。
鎖清除鎖清除是指對被檢測出不存在競爭的共享數據的鎖進行清除。
鎖清除主要通過逃逸分析來支持,如果堆上的共享數據不可能逃逸出去被其他線程訪問到,那么就可以把他們當成私有數據,也就可以將他們的鎖清除。
對于一些看起來沒有加鎖的代碼,其實隱式的加了很多鎖,例如一下的字符串拼接代碼就隱式加了鎖:
public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
String是一個不可變的類,編譯器會對String的拼接進行自動優化。在JDK1.5之前會轉化為StringBuffer對象連續append()操作。
public static String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
每一個append()方法中都有一個同步塊,虛擬機觀察變量sb,很快發現它的動態作用域被限制在concatString方法內部,也就是說,sb的所有引用永遠不會逃逸到contatString()方法之外,其他線程無法訪問到它,因此可以進行鎖清除。
鎖粗化如果一系列的操作都對同一對象反復加鎖和解鎖,頻繁的加鎖操作就會導致性能消耗。
上一節的示例代碼中連續的append()方法就屬于這種情況。如果虛擬機探測到由這樣的一串零碎的操作都是對同一個對象加鎖,將會把鎖的范圍擴展(粗化)到整個操作序列的外部。對于上一節的示例代碼就是擴展到第一個 append() 操作之前直至最后一個 append() 操作之后,這樣只需要加鎖一次就可以了。
輕量級鎖輕量級鎖跟偏向鎖是java1.6中引入的,并且規定鎖只可以升級而不能降級,這就意味著偏向鎖升級成輕量級鎖后不能降低為偏向鎖,這種策略是為了提高獲得鎖的效率。
Java對象頭通常由兩個部分組成,一個是Mark Word存儲對象的hashCode或者鎖信息,另一個是Class Metadata Address用于存儲對象類型數據的指針,如果對象是數組,還會有一部分存儲的是數據的長度。
對象頭中的Mark Word布局
輕量級鎖是相對于傳統的重量級鎖而言,它使用CAS操作來避免重量級鎖使用互斥量的開銷。對于大部分的鎖,在整個同步周期內都是不存在晶振的,因此也就不需要使用互斥量進行同步,可以先采用CAS操作進行同步,如果CAS失敗了再改用互斥量進行同步。
當嘗試獲取一個鎖對象時,如果鎖對象標記為0 01,說明鎖對象的鎖未鎖定(unlock)狀態,此時虛擬機在當前線程的虛擬機棧創建Lock Record,然后使用CAS操作將對象的Mark Word更新為Lock Record指針。如果CAS操作成功了,那么線程就獲取了該對象上的鎖,并且對象的Mark Word的鎖標記變為00,表示該對象處于輕量級鎖狀態。
如果CAS操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的虛擬機棧,如果是的話說明當前線程已經擁有了這個鎖對象,那就可以直接進入同步塊執行,否則說明這個鎖對象已經被其他線程搶占了。如果有兩個以上的線程競爭同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖。
輕量級鎖的步驟如下:
線程1在執行同步代碼塊之前,JVM會先在當前線程的棧幀中創建一個空間用來存儲鎖記錄,然后再把對象頭中的Mark Word復制到該鎖記錄中,官方稱之為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word 替換為指向鎖記錄的指針。如果成功,則獲得鎖,進入步驟(3)。如果失敗執行步驟(2)
線程自旋,自旋成功則獲得鎖,進入步驟(3)。自旋失敗,則膨脹成為重量級鎖,并把鎖標志位變為10,線程阻塞進入步驟(3)
鎖的持有線程執行同步代碼,執行完CAS替換Mark Word成功釋放鎖,如果CAS成功則流程結束,CAS失敗執行步驟(4)
CAS執行失敗說明期間有線程嘗試獲得鎖并自旋失敗,輕量級鎖升級為了重量級鎖,此時釋放鎖之后,還要喚醒等待的線程
偏向鎖偏向鎖的思想是偏向于讓第一個獲取鎖對象的線程,在之后的獲取該鎖就不再需要進行同步操作,甚至連CAS操作也不需要。
當鎖對象第一次被線程獲得的時候,進入偏向狀態,標記為1 01。同時使用CAS操作將線程ID記錄到Mark Word中,如果CAS操作成功,這個線程以后每次進入這個鎖相關的同步塊就不需要再進行任何同步操作。
當有另一個線程去嘗試獲取這個鎖對象,偏向狀態就宣告結束,此時偏向取消后恢復到未鎖定狀態或者輕量級鎖狀態。
偏向鎖獲得鎖的步驟分為:
初始時對象的Mark Word位為1,表示對象處于可偏向的狀態,并且ThreadId為0,這是該對象是biasable&unbiased狀態,可以加上偏向鎖進入(2)。如果一個線程試圖鎖住biasable&biased并且ThreadID不等于自己ID的時候,由于鎖競爭應該直接進入(4)撤銷偏向鎖。
線程嘗試用CAS將自己的ThreadID放置到Mark Word中相應的位置,如果CAS操作成功進入到3),否則進入(4)
進入到這一步代表當前沒有鎖競爭,Object繼續保持biasable狀態,但此時ThreadID已經不為0了,對象處于biasable&biased狀態
當線程執行CAS失敗,表示另一個線程當前正在競爭該對象上的鎖。當到達全局安全點時(cpu沒有正在執行的字節)獲得偏向鎖的線程將被掛起,撤銷偏向(偏向位置0),如果這個線程已經死了,則把對象恢復到未鎖定狀態(標志位改為01),如果線程還活著,則把偏向鎖置0,變成輕量級鎖(標志位改為00),釋放被阻塞的線程,進入到輕量級鎖的執行路徑中,同時被撤銷偏向鎖的線程繼續往下執行。
運行同步代碼塊
參考Java并發編程實戰
淺談Java里的三種鎖:偏向鎖、輕量級鎖和重量級鎖
CS-Notes
【Java并發編程】之十:使用wait/notify/notifyAll實現線程間通信的幾點重要說明
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71605.html
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
摘要:虛擬機所處的區域,則表示它是屬于新生代收集器還是老年代收集器。虛擬機總共運行了分鐘,其中垃圾收集花掉分鐘,那么吞吐量就是。收集器線程所占用的數量為。 本文主要從GC(垃圾回收)的角度試著對jvm中的內存分配策略與相應的垃圾收集器做一個介紹。 注:還是老規矩,本著能畫圖就不BB原則,盡量將各知識點通過思維導圖或者其他模型圖的方式進行說明。文字僅記錄額外的思考與心得,以及其他特殊情況 內存...
摘要:前言三年后端開發經驗,面的目標崗位是的高級后端開發。面試結束,應該沒有后續。 前言 三年Java后端開發經驗,面的目標崗位是20k-35k的高級后端Java開發。 第一場,基本裸面,關于曾經的項目部門答的不好,所以還是得好好準備。 某C輪在線旅游公司 筆試 先做半個小時的筆試題,一共六個題目,兩道go語言的基礎題,一道斐波那契相關,一道數據庫行列轉置,一道實現一個棧,還有一道是百萬計...
閱讀 1846·2021-11-22 15:25
閱讀 3911·2021-11-17 09:33
閱讀 2506·2021-10-12 10:12
閱讀 1801·2021-10-09 09:44
閱讀 3234·2021-10-08 10:04
閱讀 1312·2021-09-29 09:35
閱讀 1946·2019-08-30 12:57
閱讀 1301·2019-08-29 16:22