JVM內存模型和性能優化
JVM內存模型優勢html
- 內置基於內存的併發模型: 多線程機制
- 同步鎖Synchronization
- 大量線程安全型庫包支持
- 基於內存的併發機制,粒度靈活控制,靈活度高於數據庫鎖。
- 多核並行計算模型
- 基於線程的異步模型。
JVM性能的人爲問題java
- 關鍵緣由是:沒有正確處理好對象的生命週期。
- 須要從需求中找出存在天然邊界的業務對象,將其對應落實到內存中,成爲內存模型In-memory Domain Model。
- 有大小邊界限制的內存是緩存,沒有永遠使用不完的內存,緩存=「有邊界的」內存。
- 緩存是Domain Model對象緩存,不一樣於傳統意義上數據庫緩存的定義。
- 分佈式緩存能夠提升巨量數據處理計算能力。
Java內存種類git
- Stack棧內存
存取速度快,數據可多線程間共享。
存在棧中的數據大小與生存期必須肯定
- Heap堆內存
大小動態變化,對象的生命週期沒必要事先告訴編譯器JVM。
兩種內存使用github
- Stack棧內存
基本數據類型,Java 指令代碼,常量
對象實例的引用 對象的方法代碼
- Heap堆內存
對象實例的屬性數據和數組。堆內存由Java虛擬機的自動垃圾回收器來管理。
對象如何保存在內存中?算法
- 對象的屬性Attribute Property
屬性值做爲數據,保存在數據區heap 中,包括屬性的類型Classtype和對象自己的類型數據庫
- 方法method
方法自己是指令的操做碼,保存在stack中。
方法內部變量做爲指令的操做數也是在Stack中,
包括基本類型和其餘對象的引用。數組
- 對象實例在heap 中分配好內存之後,須要在stack中保存一個4字節的heap內存地址,用來定位該對象實例在heap 中的位置,便於找到該對象實例。
靜態屬性和方法的特色緩存
- 靜態屬性和方法都是保存在Stack中,
- Stack內存是共享的,其餘線程均可以訪問靜態屬性實際是全局變量。
- 靜態方法在Stack,就沒法訪問Heap中的數據。靜態方法沒法訪問普通對象中數據。
- 靜態屬性意味着全局變量,生命週期和JVM一致。JVM屬於技術邊界,靜態只能用於技術邊界內工具性質使用,不能用做業務。
內存管理:垃圾回收機制安全
- 每一種垃圾收集的算法(引用計數、複製、標記-清除和標記-整理等)在特定條件下都有其優勢和缺點。
- 當有不少對象成爲垃圾時,複製能夠作得很好,可是複製許多生命週期長的對象時它就變得很糟(要反覆複製它們)。
- 標記-整理適合生命週期長對象能夠作得很好(只複製一次),可是不適合短生命的對象。
- Sun JVM 1.2 及之後版本使用的技術稱爲 分代垃圾收集(generational garbage collection),它結合了這兩種技術以結合兩者的長處。
可選用的GC類型性能優化
JVM性能優化
- 內存微調優化
- 鎖爭奪微調:
多線程 不變性 單寫原則 Actor Disrupotor
- CPU使用率微調
- I/O 微調
內存微調優化
- 內存分配:
新生代 Eden和survior 舊生代內存大小分配。
內存越大,吞吐量越大,可是須要內存整理的時間就越長,響應時間有延遲。
- 垃圾回收機制
垃圾回收啓動整個應用都暫停,暫停時間形成響應時間有延遲。
內存微調目標
- 在延遲性(響應時間)和吞吐量上取得一個平衡。
- 內存大小影響吞吐量和延遲性。須要在內存大小和響應時間之間取得一個平衡。
- 垃圾回收機制是延遲的最大問題。目標儘可能不啓動,少啓動。
內存模型
新生代Eden內存分配
- 新生代(New Generation ):Eden + 1 Survivor。全部新建立的對象在Eden。
- 當Eden滿了,啓動Stop-The-World的GC,或爲minor gc,採起數次複製Copy-Collection到Survivor。
- 通過幾回收集,壽命不斷延長的對象從Survivor 進入老生代,也稱爲進入保有Tenuring,相似普通緩存LRU算法。
survivor設計要旨
- 足夠大到能容納全部請求響應中涉及的對象數據。
- 每一個survivor空間也要足夠大到可以容納活躍的請求對象和保有對象。
- Survivor大小決定了保有Tenuring閥值,閥值若是能大到容納全部常住對象,那麼保有遷移過程就越快。
老生代Old
- 老生代的gc稱爲major gc,就是一般說的full gc。
- 採用標記-整理算法。因爲老年區域比較大,並且一般對象生命週期都比較長,標記-整理須要必定時間。因此這部分的gc時間比較長。
- minor gc可能引起full gc。當eden+from space的空間大於老生代的剩餘空間時,會引起full gc。這是悲觀算法,要確保eden+from space的對象若是都存活,必須有足夠的老生代空間存放這些對象。
- 這些都根據狀況調整啓動JVM的設置。
- 使用 Adaptive讓JVM自動劃分新生代和老生代。
Permanent Generation 永久代
- 該區域比較穩定,主要用於存放classloader信息,好比類信息和method信息。
- 缺省是 64M ,若是你的代碼量很大,容易出現OutOfMemoryError: PermGen space 。
- 2G以上內存設置MaxPermSize爲160M
- -XX:PermSize=128m -XX:MaxPermSize=160m
下降Full GC發生機率
- 爲了下降Full GC發生機率,若是下降了老生代大小,那麼 OutOfMemoryError 發生,Full GC機率反而會上升。
- 若是爲了下降Full GC,增長老生代大小,執行時間可能會被延長。
- 必須尋找合適大小的老生代。
- 避免大的對象遷移到老生代。
- 減小遷移到老生代的對象數目
java.lang.OutOfMemoryError
- (1)在高負荷的狀況下的卻須要很大的內存,所以能夠經過修改JVM參數來增長Java Heap Memory。
- (2)應用程序使用對象或者資源沒有釋放,致使內存消耗持續增長,關鍵採起OO封裝邊界方式,樹立對象都有生命週期的基本習慣。
- (3)再一種也多是對於第三方開源項目中資源釋放了解不夠致使使用之後資源沒有釋放(例如JDBC的ResultSet等)。
JVM參數
- -Xms, -Xmx—定義JVM的heap大小最小和最大值。
- -XX:NewSize— 定義年輕態的最小大小,Eden越大越好,可是越大響應有延遲。
- -Xmx2G -Xms1G -XX:NewSIze=512M (OldGen at least 1G)
- -Xmx3G -Xms1G -XX:NewSize=512M (OldGen at least 2G)
- Xmx4G -Xms2G -XX:NewSize=1G (OldGen at least 2.5G)
- -Xmx6G -Xms3G -XX:NewSize=2G (OldGen at least 3.5G)
- -Xmx8G -Xms4G -XX:NewSize=3G (OldGen at least 4.5G)
參數調整示意
- JAVA_OPTS="$JAVA_OPTS -server -Xss1280K -Xms1664m -Xmx1664m -XX:MaxPermSize=128m -XX:SurvivorRatio=16 -XX:NewSize=1280m -XX:MaxNewSize=1280m -XX:+DisableExplicitGC -XX:GCTimeRatio=2 -XX:ParallelGCThreads=4 -XX:+UseParNewGC -XX:MaxGCPauseMillis=2000 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled
Survivor大小
- NewSize / ( SurvivorRatio + 2)
- 若是SurvivorRatio =16, NewSize =1280m,那麼S大小是70M。
- 過小,溢出的複製Collection進入老生代。
- 太大,閒置無用 浪費內存。
- 使用XX:+PrintTenuringDistribution 和-XX:+PrintGCDetails, -XX:+PrintHeapAtGC觀察:
- 與 -XX:+UseAdaptiveSizePolicy 衝突
垃圾回收機制啓動
- 垃圾回收機制不會頻繁啓動,由於機制一旦啓動,形成應用程序停頓。
- 機制通常內存剩餘5%左右啓動,因此有現象:啓動服務器,內存不斷消耗,有多大內存消耗多大。
- 問題:若是服務器程序頻繁觸及5%底線,機制頻繁啓動,形成服務器慢..甚至死機。
- 根源:應用程序無限制頻繁大量建立對象,消耗內存。
控制垃圾回收
- 帶CMS參數的都是和併發回收相關的
- -XX:+UseParNewGC,對新生代採用多線程並行回收。
- CMSInitiatingOccupancyFraction=90說明年老代到90%滿的時候開始執行對年老代的併發垃圾回收(CMS)
- 用jmap和jstack查看
串行 並行回收的區別
- 新生代 高吞吐量:
- -XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseParNewGC
- 老生代 低暫停:
- -XX:+UseParallelOldGC
-XX:+UseConcMarkSweepGC
- 相同點:GC時都暫停一切。
- 不一樣點:一個線程和多個線程同時GC
並行和CMS(Concurrent-Mark-Sweep)區別
- CMS步驟:
- - initial mark
- concurrent marking
- remark
- concurrent sweeping
- 區別:CMS一個線程,並行多個線程
- CMS只是在1 3階段暫停,而並行所有暫停。
Parallel GC 和 CMS GC
- 壓實compaction是移除內存碎片,也就是移除已經分配的內存之間的空白空間。
- 在Parallel GC中,不管Full GC是否執行,壓實老是被執行,會花費更多時間,不過在執行完Full GC後,內存也許再被使用時,會分配得快些,可以順序分配了。
- CMS GC 並不執行壓實,因此更快,碎片太多,沒有空間放置大的須要連續空間的對象,「Concurrent mode failure」會發生。
並行和CMS配置
- -XX:UserParNewGC 適合於
新生代 (multiple GC threads)
-XX:+UseConcMarkSweepGC 適合於
老生代 (one GC thread, freezes the JVM only during the initial mark and remark phases)
-XX:InitiatingOccupancyFraction 80是表示CMS是在老生代接近滿80%啓動,如CPU空閒,可設定點一些。
-XX:+CMSIncrementalMode 用於CMS,不會讓處理器Hold住整個併發phases 。
高吞吐量調整
- UseParallelGC 和UseParNewGC等高吞吐量配合參數:
- -XX:+UseAdaptiveSizePolicy
- -XX:MaxGCPauseMillis=… (i.e. 100)
- -XX:GCTimeRatio=… (i.e. 19)
UseAdaptiveSizePolicy
- 當使用-XX:+UseParallelGC 缺省策略加載,XX:+UseAdaptiveSizePolicy。
- 主要調整下面參數,在暫停和吞吐量之間取得一個平衡:
- 一個合適的最大GC暫停值
- 一個合適的應用程序吞吐量值
- 最小化實現路徑。
UseAdaptiveSizePolicy 策略路徑
- 若是GC暫停時間大於目標暫停時間(-XX:MaxGCPauseMillis=nnn ),下降新生代大小以匹配目標暫停時間。
- 若是暫停時間合適,考慮應用的吞吐量,經過增大新生代的大小知足吞吐量。
- 若是暫停時間和吞吐量兩個目標都知足,新生代大小下降以節約成本路徑。
UseAdaptiveSizePolicy
- -XX:MaxGCPauseMillis=nnn :不能設置太小,會阻礙吞吐量,若是不設置,暫停時間依賴heap中活動數據量。
- -XX:GCTimeRatio=nnn 不超過應用運行時間的1 / (1 + nnn) 用在垃圾回收上。缺省99。垃圾回收時間不該該超過總體時間的1%
JVM微調調試方法
- 配置JVM的JAVA_OPTS參數 –verbosegc
- 觀察Full GC的信息輸出:
- [Full GC $before->$after($total), $time secs]
- Full GC太頻繁,應用暫停,響應時間受影響。
- 克服GC太頻繁方法:
- 1. 增大內存。增大年輕代的內存
- 2.使用LRU等緩存,限制大量對象建立。
- 3. 64位下壓縮對象頭。
- 消滅Full GC:-XX:+PrintGCDetails 無Full GC輸出
內存大小影響
- 大內存:
1. 下降GC執行次數。
2.增長每次GC執行的時間。
- 小內存:
1.增長了GC執行次數
2.下降每次GC執行的時間。
若是Full GC可以在1秒內完成,10G也是合適的。
Jstat 監視微調
- jstat -gcutil 21891 250 7
- 21891是Java的pid, 250表示間隔幾秒 7表示採樣7次
- S0 S1 E O P YGC YGCT FGC FGCT GCT
12.44 0.00 27.20 9.49 96.70 78 0.176 5 0.495 0.672
12.44 0.00 62.16 9.49 96.70 78 0.176 5 0.495 0.672
12.44 0.00 83.97 9.49 96.70 78 0.176 5 0.495 0.672
0.00 7.74 0.00 9.51 96.70 79 0.177 5 0.495 0.673
0.00 7.74 23.37 9.51 96.70 79 0.177 5 0.495 0.673
0.00 7.74 43.82 9.51 96.70 79 0.177 5 0.495 0.673
0.00 7.74 58.11 9.51 96.71 79 0.177 5 0.495 0.673
- Minor GC :YGC年輕代GC發生了78次,YGCT是GC發生的時間累計0.176。
- FULL GC發生了5次,累計0.495, 每次是0.495/5
- http://www.cubrid.org/blog/dev-platform/how-to-monitor-java-garbage-collection/
- 若是GC執行時間在一秒以上,須要GC微調,若是在0.1-0.3之間則不須要
須要微調的案例
- Full GC超過一秒,須要微調。
- Minor GC正常
微調前檢查內存大小分配
- jstat –gccapacity
- NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
- 212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5
- 新生代是 212,992 KB,老生代是1,884,160 KB
- 新生代:老生代是1:9, 調整NewRatio
- NewRatio=2
- NewRatio=3
- NewRatio=4
- 若是其中一個設置沒有FULL GC發生,就是合適新生代和老生代的大小。
- 隨着新生代內存減少,其GC時間縮短:
- NewRatio=2: 45 ms
- NewRatio=3: 34 ms
- NewRatio=4: 30 ms
- 內存輸出結構:
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219
- 年輕代發生了2424次,而FullGC沒有一次發生,存在大量臨時對象都是新生代毀滅。
Jstat參數說明
JVM優化參數
- JAVA_OPTS="$JAVA_OPTS -verbose:gc -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -Xloggc:/home/jdon/jdongc.log -server -Xms1536m -Xmx1664m -XX:NewSize=768m -XX:MaxNewSize=896m -XX:+UseAdaptiveGCBoundary -XX:MaxGCPauseMillis=250 -XX:+UseAdaptiveSizePolicy -XX:+DisableExplicitGC -XX:ParallelGCThreads=4 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled
- 最大新生代GC暫停時間是250毫秒,在這個基礎上JVM自動調整儘可能知足吞吐量。
- [GC 2016.468: [ASParNew: 686991K->42743K(711316K), 0.1310080 secs] 713706K->74888K(1422668K) icms_dc=0 , 0.1324500 secs]
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 0.00 64.80 93.53 4.52 66.34 7 0.619 7 2.381 3.000
若是響應時間仍是不夠快?
- 響應延遲和吞吐量是一對矛盾,而吞吐量主要標誌是內存大小。
- 下降NewSize大小, 下降survivor空間。
- 下降進入老生代reduce的門檻,由於緩存Hold住大量長生命對象,讓這些對象進口進入老生代。而老生代的CMS不多暫停。
CMS
- CMS並不進行內存壓實compact,因此,會致使碎片。而碎片也會致使暫停。
- Apache Cassandra使用 slab allocator。
- 每一個Slab是2M;
- 用compare-and-set拷貝他們。
- 三天
G1 vs CMS vs Parallel GC
三個垃圾回收機制的比較,語法以下:
- -XX:+UseParallelOldGC
- -XX:+UseConcMarkSweepGC
- -XX:+UseG1GC
使用GCViewer觀察,結果以下 單位是毫秒:
|
Parallel |
CMS |
G1 |
Total GC pauses |
20 930 |
18 870 |
62 000 |
Max GC pause |
721 |
64 |
50 |
並行GC ( - XX:+ UseParallelOldGC ) 。在30多分鐘的測試時間內,用並行收集器 GC暫停花了接近21秒。最長停頓了721毫秒。所以,咱們以此爲基準: GC下降了吞吐量爲總運行時間的1.1 %。最壞狀況下的延遲爲721ms 。
CMS ( - XX:+ UseConcMarkSweepGC ) 一樣30分鐘,由於GC咱們失去了不到19秒 。相比並行GC吞吐量明智的。延遲另外一方面已顯著改善 - 最壞狀況下的延遲時間減小10倍以上!來自GC的最大暫停時間咱們如今面臨的只是64ms。
最閃亮的GC算法 - G1 ( - XX:+ UseG1GC ) 。在一樣的測試,,咱們看到的結果吞吐量問題比較嚴重。這一次,咱們的應用程序花費超過一分鐘等待GC來完成。比較這與CMS中的開銷只有1% ,咱們如今面對接近的吞吐量3.5 %的效果。但若是你真的不關心的吞吐量,而是想擠出最後那麼一點延遲 - 比CMS提升20%左右 - 用G1看到的最長GC暫停服用僅50毫秒。
結論:CMS仍然是最好的「默認」選項。 G1的吞吐量仍然差那麼多,獲得的延遲一般是不值得的。
JVM推薦設置
Heap size |
Total GC pauses GC暫停 |
Throughput吞吐量 |
-Xmx300m |
207.48s |
92.25% |
-Xmx384m |
54.13s |
97.97% |
-Xmx720m |
20.52s |
99.11% |
-Xmx1,440m |
*11.37s |
*99.55%
|
對象和Java多線程
- 缺省對象都是繼承java.lang.Object
- 也能夠特別繼承java.lang.Thread ;
- 或實現java.lang.Runnable接口
- 對象的方法以線程方式執行。
線程的主內存和工做內存
- 主內存對於全部線程可見的。主內存通常在Heap中,對象的屬性值是放在Heap中。
- 每條線程都有本身的工做內存。工做內存裏的變量,在多核處理器下,將大部分儲存於處理器高速緩存中。
- 工做內存會拷貝主存中的變量,而後對變量的操做在本身的工做內存中進行。
- 線程之間沒法相互直接訪問,變量傳遞均須要經過主存完成。
問題?
如何保證內存計算一致性
- 緩存一致性
當一個線程更新了本身工做內存中的數據後,沒有寫到主內存,其餘線程是不知道的。
(1)順序一致性模型:
要求對改變的值當即進行傳播, 並確保該值被全部其餘線程接受後, 才能繼續執行其餘指令.
(2) 釋放一致性模型:
容許線程將改變的值延遲到鎖釋放時才進行傳播.
happens-before ordering
- 1.獲取對象監視器的鎖(lock)
- 2. 清空工做內存數據, 從主存複製變量到當前工做內存, 即同步數據 (read and load)
- 3. 執行代碼,改變共享變量值 (use and assign)
- 4. 將工做內存數據刷回主存 (store and write)
- 5. 釋放對象監視器的鎖 (unlock)
happens-before ordering實現
- final 永不改變
- volatile 標註被改變的值爲原子性
- JVM優化的鎖java.util.concurrent.locks包java.util.concurrent.atmoic包
- synchronized 堵塞鎖
- 如何選用這些工具呢?前提是保證線程安全性。
線程安全模式
- 線程安全性的定義要求不管是多線程中的時序或交替操做,都要保證不破壞業務自己不變約束 。
- 爲了保護狀態的一致性,要在單一的原子操做中更新相互關聯的狀態變量。
- 設計線程安全的類時,優秀的面向對象技術——封裝、不可變性以及明確的不變約束——會給你提供諸多的幫助。
- 無狀態對象永遠是線程安全的
線程安全模式
- 儘可能不使用synchronized鎖,鎖是耗費資源和性能的。
- 首先 編寫那些不用任何特別處理的線程安全代碼,好比不變性代碼。
- 使用producer-observer模式。
- 其次:使用Visibility 使資料對全部線程可見。
- 最後:使用JVM優化的鎖。
單值更新
- 使用Atmoic原子特性API:
- Atomic{Integer|Long}.compareAndSet().
- 使用CAS實現機制的API。
- AtomicReference.compareAndSet()實現不變性對象內部的組合更新。
immutable 不可變模式
- Immutable是當被構造之後就再也不改變。
- Immutable 的對象老是線程安全。
- 特徵:
- 1. 構造之後就不會改變;
- 2. 全部字段是 final;
- 3. 它是正常構造。
- 發佈一個Immutable對象是安全的。
Publishing發佈公開對象
- public static Set<Secret> knownSecrets;
- public void initialize() {
- knownSecrets = new HashSet<Secret>();
- }
- 因爲外界能夠訪問knownSecrets 而且修改,那麼knownSecrets 至關於脫離當前對象的scope生命週期,變成escaped 逃脫了。
安全的發佈公開對象模式
- 發佈表明:引用這個對象而且這個對象中狀態必須同時爲其餘人可見的,經過以下方式發佈:
- 1.從一個靜態初始期中賦予一個對象引用;
- public static Holder holder = new Holder(42);
- 2. 將引用賦予一個 volatile 或者 AtomicReference字段;
- 3. 將引用賦予一個 final字段,而且構造後不改變(不變性); or
- 4.將引用賦予一個 字段被適當的鎖守衛。
自然的線程安全
- Hashtable, synchronizedMap, or Concurrent-Map
- Vector, CopyOnWriteArrayList, CopyOnWrite-ArraySet, synchronizedList, or synchronizedSet
- BlockingQueue or a ConcurrentLinkedQueue
Visibility/NoVisibility模式
- 線程更新的是本身私有空間變量,須要更新到主內存空間,一個線程修改的結果對於另一個線程是NoVisibility :
- class RealTimeClock {
- private int clkID;
- public int clockID() { return clkID; }
- public void setClockID(int id) { clkID = id; } }
- Thread 1 calls the setClockID method, passing a value of 5.
- Thread 2 calls the setClockID method, passing a value of 10.
- Thread 1 calls the clockID method, which returns the value 5.
- 出現明明修改某個字段值,可是刷新仍是舊值。
多線程訪問同一資源
- 1. 使用synchronized
- 2. 將clkID 標爲volatile
- 使用synchronized 壞處:排他性鎖定,影響性能。
- 使用JDK5 ReentrantReadWriteLock
volatile
- 不是很健壯的鎖機制,適合必定條件:
- 1. 寫變量值不依賴它當前值,好比:直接this.xxx = xxx;包括volatile bean
- 2.這個變量不參與其餘變量的不變性範圍。
- 做爲標識完成、中斷、狀態的標記使用
- 加鎖能夠保證可見性與原子性(「讀-改-寫」 原子操做 );volatile變量只能保證可見性。
- 相關文章:http://www.ibm.com/developerworks/java/library/j-jtp06197.html
Volatile缺點
- @NotThreadSafe
- public class NumberRange {
- private int lower, upper;
- public int getLower() { return lower; }
- public int getUpper() { return upper; }
- public void setLower(int value) {
- if (value > upper) throw new IllegalArgumentException(...);
- lower = value; }
- public void setUpper(int value) {
- if (value < lower) throw new IllegalArgumentException(...);
- upper = value; } }
Volatile缺點
- 初始值是(0, 5)
- 線程A: setLower(4)
- 線程B: setUpper(3)
- 結果是 (4, 3) , 而這一結果根據setLower和setUpper中if邏輯判斷是不可能獲得的。
- 這時須要synchronization
- 或使用final替代Volatile
使用final替代Volatile
- 若是須要修改,更換整個對象,值對象定義
原子操做模式
- 只是將變量做爲可見仍是不夠,還要對操做這些變量的操做方法保證原子性。
- 假設有操做A和B,若是從執行A的線程的角度看,當其餘線程執行B時,要麼B所有執行完成,要麼一點都沒有執行,這樣A和B互爲原子操做。一個原子操做是指:該操做對於全部的操做,包括它本身,都知足前面描述的狀態。
- 爲了確保線程安全,「檢查再運行」操做(如惰性初始化)和讀-改-寫操做(如自增)必須是原子操做。咱們將「檢查再運行」和讀-改-寫操做的所有執行過程看做是複合操做:爲了保證線程安全,操做必須原子地執行。
鎖模式
- synchronized塊:內部鎖(intrinsic locks)或監視器鎖(monitor locks)
- 執行線程進入synchronized塊以前會自動得到鎖。
- 進入這個內部鎖保護的同步塊或方法。
- 內部鎖在Java中扮演了互斥鎖 。意味着至多隻有一個線程能夠擁有鎖,可能發生死鎖,執行synchronized塊的線程,不可能看到會有其餘線程能同時執行由同一個鎖保護的synchronized塊。
- 它徹底禁止多個用戶同時使用 ,性能問題
重進入(Reentrancy)
- 當一個線程請求其餘線程已經佔有的鎖時,請求線程將被阻塞 。線程在試圖得到它本身佔有的鎖時,請求會成功 .
- public class Widget {
- public synchronized void doSomething() {
- }}
- public class LoggingWidget extends Widget {
- public synchronized void doSomething() {
- System.out.println(toString() + ": calling doSomething");
- super.doSomething();
- }}
Reentrancy好處
- 子類覆寫了父類synchronized類型的方法,並調用父類中的方法。若是沒有可重入的鎖,這段看上去很天然的代碼就會產生死鎖。
cheap read-write lock
- public class CheesyCounter {
- // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this")
- private volatile int value;
- public int getValue() { return value; }
- public synchronized int increment() {
- return value++;
- }
- }
ReentrantReadWriteLock
- 適合場景:大量併發讀操做,少許甚至一個線程作修改。
- 優勢:克服synchronization跨多個方法沒法重入的問題(容易發生死鎖),好比 在一個地方lock,而在另一個地方 unlock.
- public void set(String key, String value) {
- write.lock();
- try {dictionary.put(key, value);}
- finally {write.unlock();}
- }
- public String get(String key) {
- read.lock();
- try {return dictionary.get(key);}
- finally {read.unlock();}
- }
什麼時候用
- 若是須要timed, polled, 或可中斷 lock, fair queueing, non-block-structured locking.就是要ReentrantReadWriteLock
- 不然使用 synchronized.
案例:如何實現集合的邊讀邊改
- 聯繫人名單集合,發送Email
- public void sendMessages(Map contactMap) {
- sendEmail(contactMap.values());
- }
- contactMap是Contact集合,contactMap.values是遍歷contactMap中元素Contact對象。
- 假設:若是在遍歷發生Email同時,有新的Contact對象加入到contactMap集合中,這時會拋出併發錯誤。
設計新的不可變集合
使用新不可變集合類
狀態和值對象
- 值對象是DDD中一種模型,不可變性。
- 狀態是表達一段時間內一個邏輯爲真的事實,狀態是不可變的,由於咱們不能回到過去改變狀態。
- 狀態是一種值對象。
- 經過不變性規避了共享鎖的爭奪,從而得到了更好的併發性能。
- 具體案例見jivejdon中的ForumState等
ThreadLocal
- ThreadLocal能夠維持線程的封閉性,一個請求一個線程,至關於request.setAttribute/getAttribute;
- ThreadLocal能夠爲每一個線程保存本身的狀態值。
- ThreadLocal的get/set方法爲每一個線程維持一份獨立的分離的狀態值。Get方法可以返回最新經過set方法保存的狀態值
- 常常被框架使用。如Spring Hibernate
數據庫鏈接放入ThreadLocal
- private static ThreadLocal<Connection> connectionHolder =
- new ThreadLocal<Connection>() {
- public Connection initialValue() {
- return DriverManager.getConnection(DB_URL); }
- } ;
- public static Connection getConnection() {
- return connectionHolder.get();
- }