深入理解 Synchronized
同步
synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變量的內存可見性
Java中每一個對象都可以作為鎖,這是synchronized實現同步的基礎。
synchronized 常見的三種用法如下:
通過如下代碼來分析下synchronized 獲取的是哪個對象的鎖
public class SynTest {private static List<String> list = new ArrayList<String>();//當前實例的鎖public synchronized void add1(String s){list.add(s);}//SynTest.class 鎖public static synchronized void add2(String s){list.add(s);}//SynTest.class 鎖public void add3(String s){synchronized(SynTest.class){list.add(s);}}//當前實例的鎖public void add4(String s){synchronized(this){list.add(s);}} }普通同步方法,鎖是當前實例對象
add1 方法是synchronized的第一種用法,因為它是普通同步方法,所以獲取當前實例的鎖。
add2 方法是synchronized的第二種用法,因為它是靜態同步方法,所以獲取SynTest.class的鎖。
add3 方法是synchronized的第三種用法。指定鎖:SynTest.class。
add4 方法是synchronized的第三種用法。指定鎖:this(當前實例)。
結論:
add1和add4方法的鎖都是 當前實例,所以add1和add4 可以實現方法互斥
add2和add3方法的鎖都是SynTest.class,所以add2和add3可以實現方法互斥。
**注意:**add1和add2兩個synchronized是不互斥的,因為他們不是同一把鎖。只有同一把鎖才會互斥。
synchronized塊編譯成字節碼后會在同步塊的入口位置和退出位置分別插入monitorenter和monitorexit字節碼指令。如下圖:
monitorenter和monitorexit指令規則:
1. monitorenter在編譯后插入到同步代碼庫的開始位置。
2. monitorexit插入在方法結束處和異常處。
3. 一個monitorenter必須保證有對應的monitorexit。
4. 任何對象都有一個monitor以之關聯,當一個monitor被持有后,該對象出于鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,嘗試獲取對象的鎖。
靜態方法和普通方法在編程成字節碼后會在方法的訪問標識字段中標識為同步方法。
方法同步的細節在JVM規范里并沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。
需要提前了解知識點:java內存模型
內存可見性
synchronized關鍵字強制實施一個互斥鎖,使得被保護的代碼塊在同一時間只能有一個線程進入并執行。當然synchronized還有另外一個 方面的作用:在線程進入synchronized塊之前,會把工作存內存中的所有內容映射到主內存上,然后把工作內存清空再從主存儲器上拷貝最新的值。而 在線程退出synchronized塊時,同樣會把工作內存中的值映射到主內存,但此時并不會清空工作內存。這樣一來就可以強制其按照上面的順序運行,以 保證線程在執行完代碼塊后,工作內存中的值和主內存中的值是一致的,保證了數據的一致性!
所以由synchronized修飾的set與get方法都是相當于直接對主內存進行操作,不會出現數據一致性方面的問題。
指令重排序
指令重排序是JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,盡可能地提高并行度。
synchronized塊對應java程序來說是原子操作,所以說內部不管怎么重排序都不會影響其它線程執行導致數據錯誤。執行的指令也不會溢出方法。
happens-before
監視器鎖規則:對一個監視器鎖的解鎖,happens- before 于隨后對這個監視器鎖的加鎖。
鎖優化
在多線程并發編程中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖。在Java1.5中,synchronize是性能低效的。因為這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化余地。
偏向鎖
簡單的理解,偏向于這個線程。 當一個線程進入同步塊,首先測試對象的mark word 中的threadId是否等同當前線程ID,如果成功,則獲取鎖成功,否則。首先測試mark word中的鎖標識是否為1 。如果沒有設置。則升級為輕量級鎖。否則 通過CAS操作,將自己的線程ID 嘗試放入 的mark word 的位置。如果成功則獲取鎖成功。否則,說明對象存在競爭。將會在適當的地方掛起獲取鎖的線程。然后升級鎖。接著執行代碼。
偏向鎖默認為開啟狀態。只是會在應用啟動后延遲啟動通過參數可以設置不延時XX:BiasedLockingStartupDelay=0。如果你確定應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態。
輕量級鎖
JVM通過CAS修改對象頭來獲取鎖,如果CAS修改成功則獲取鎖,如果獲取失敗,則說明有競爭,則通過CAS自旋一段時間來修改對象頭,如果還是獲取失敗,則升級為重量級鎖。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。
重量級鎖
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴于底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。
鎖消除:
鎖消除是指虛擬機在即時編譯器在運行時,對于一些在代碼上要求同步,但是被檢測到不可能存在數據競爭的鎖進行消除。比如,一個鎖不可能被多個線程訪問到,那么在這個鎖上的同步塊JVM將把鎖消除掉。
鎖粗化
程序中一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。
for(int i=0; i<1000; i++){synchronized(this){...} }上面代碼JVM將會優化成如下:
synchronized(this){for(int i=0; i<1000; i++){...} }本人簡書blog地址:http://www.jianshu.com/u/1f0067e24ff8????
點擊這里快速進入簡書
總結
以上是生活随笔為你收集整理的深入理解 Synchronized的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Semaphore 源码分析
- 下一篇: 最简单的 java 防反编译技巧