synchronized 的實現方式是用 Monitor 進行加鎖,這是一種互斥鎖,為了表示他對性能的影響我們稱之為重量級鎖。
Java 的線程是映射到操作系統原生線程之上的,要阻塞或喚醒一個線程就需要操作系統的協助,讓線程從用戶態轉換到內核態,而狀態轉換需要耗費 CPU 很多的時間。
鎖優化僅在 Java 虛擬機 server 模式下起作用

自旋鎖
Java 虛擬機的開發工程師們在分析過大量數據后發現:共享數據的鎖定狀態一般只會持續很短的一段時間,為了這段時間去掛起和恢復線程其實並不值得。
自旋鎖在 JDK 1.4 中引入,在 JDK 1.6 中默認開啟。
自旋等待雖然避免了線程切換的開銷,但自旋的線程要占用處理器時間的,所以若鎖被占用的時間很短,自旋等待的效果就會非常好,反之鎖被占用的時間很長,那么自旋的線程只會白白消耗 CPU 資源。
因此自旋等待的時間必須要有一定的限度,超過限定的次數仍然沒有成功獲得鎖,就應當掛起(阻塞)線程了。自旋次數的默認值是 10 次。
自適應自旋鎖
在 JDK 1.6 中引入了自適應自旋鎖。
自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個循環。
如果對於某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。
鎖消除
在動態編譯同步塊的時候,JIT 編譯器可以借助一種被稱為逃逸分析(Escape Analysis)的技術來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程。從而取消對這部分代碼的同步。
// 實際代碼,hollis 的引用不會“逃逸”到 f()方法之外,其他線程無法訪問到它,鎖也就沒有了意義 public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } } // JIT 編譯后代碼 public void f() { Object hollis = new Object(); System.out.println(hollis); } // 和上面例子相同,sb 的作用域其他線程無法訪問,append 方法的同步會被 JIT 編譯消除掉 public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
鎖粗化
當 JIT 編譯器發現一系列連續的操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作出現在循環體中的時候,會將加鎖同步的范圍擴散(粗化)到整個操作序列的外部。
在編寫代碼的時候,總是推薦將同步塊的作用范圍(鎖粒度)限制得盡量小(只在共享數據的實際作用域中才進行同步),這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程可以盡快的拿到鎖。
鎖粒度:不要鎖住一些無關的代碼。鎖粗化:可以一次執行完的不要多次加鎖執行
public Object object = new Object(); // 源代碼 public void f() { for (int i = 0; i < 100000; i++) { synchronized (object) { System.out.println("xx"); } } } // JIT 編譯后代碼 public void f() { synchronized (object) { for (int i = 0; i < 100000; i++) { System.out.println("xx"); } } } public StringBuffer sb = new StringBuffer(); // 和上面例子一樣,會把 append 操作的鎖提到上一層,讓三個 append 操作只加一次鎖 public String concatString(String s1, String s2, String s3) { sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
https://www.jianshu.com/p/f05423a21e78
