Java---------synchronized鎖的優化

JDK1.6之後對於synchronized鎖的優化

如果想了解synchronized的底層實現,以及用法請參考我的上一篇及上上(或者上上上一篇???)一篇博客。

在JDK1.5中,synchronized是性能低效的。因爲這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起 線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java 提供的Lock對象,性能更高一些。

到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏 向鎖等等。導致在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronized,在未來 的版本中還有優化餘地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行 同步。

 synchronized優化 :

瞭解了Synchronized的底層實現就知道,它最大的特徵就是在同一時刻只有一個線程能夠獲得對象的監視器 (monitor),從而進入到同步代碼塊或者同步方法之中,即表現爲互斥性(排它性)。這種方式肯定效率低下, 每次只能通過一個線程,既然每次只能通過一個,這種形式(互斥:保證安全)不能改變的話,那麼我們能不能讓每次通過的速度變快 一點呢。

1、CAS操作(無鎖實現的同步—樂觀鎖)-自旋

之前synchronized獲取鎖失敗是將線程掛起-悲觀鎖策略(假設只要當前線程進入同步方法或者同步代碼塊,一定有人競爭,所以掛起。直到有人通知我)

public class Test implements Runnable{

private int i=0;

public synchronized void setI(){

System.out.println(Thread.currentThread().getName()+"----"+i);

this.i=10;

System.out.println(Thread.currentThread().getName()+"----"+i);

}

public static void main(String[]args){

Runnable runnable=new Test();

Thread thread1=new Thread(runnable);                       

Thread thread2=new Thread(runnable);

Thread thread3=new Thread(runnable);

thread1.start();

thread2.start();

thread3.start();

}

public void run(){

    System.out.println(Thread.currentThread().getName()+"----"+i);

setI();

}

}

 

Compare And Swap(O,V,N)

O 預期的值 (舊值);

V 內存地址存放的實際值;

N 更新的新值

當O==V時,此時表示沒有線程修改共享變量的值,此時可以成功的將內存中的值修改爲N

當O!=V時,此時表示內存中共享變量的值已被其他線程修改,此時返回將內存中的最新值V,再次嘗試修改變量

For/while 循環,減少了掛起阻塞操作

自旋帶來的問題

1、ABA問題:

解決方法:添加版本號,比較的時候不僅比較值,還要比較版本號

i = 0(初始化)

 

線程1

線程2

線程3

 

 

 

i= 1

I = 0

i= 0

 

 

 

2、自旋在CPU上跑無用指令,會浪費CPU資源

解決方法:JVM嘗試自旋一段時間,若在此時間段內,線程成功獲取到鎖,再下次獲取鎖時,適當延長自旋時間

若在此時間段內,線程沒有獲取到鎖,再下次獲取鎖時,適當減少自旋時間

3、公平性問題:處於阻塞態線程比處於自旋狀態的鎖更慢獲取到鎖

處於阻塞狀態的鎖可能一直無法獲取到鎖

解決方案:只有LOCK鎖可以實現公平性,synchronized無法實現公平鎖

1、偏向鎖:最樂觀的一種鎖(進入同步方法或同步塊的始終是一個線程)

JDK1.6之後默認synchronized。

當出現另一個線程也嘗試獲取鎖(在不同的時刻)時,偏向鎖會升級爲輕量級鎖

2、輕量級鎖

不同時刻有不同的線程嘗試獲取鎖,“由於深夜十字路口的車輛來往可能比較少,如果還設置紅 綠燈交替,那麼很有可能出現四個方向僅有一輛車在等紅燈的情況。

因此,紅綠燈可能被設置爲閃黃燈的情況,代表車輛可以自由通過,但是司機需要注意觀察。

同一時刻有不同的線程嘗試獲取鎖,會將偏向鎖升級爲重量鎖

3、重量級鎖

JDK1.6之前的synchronized都是重量級鎖,就是將線程阻塞掛起

鎖只有升級,沒有降級

 

Java虛擬機中synchronized關鍵字的實現,按照代價由高到低可以分爲重量級鎖、輕量鎖和偏向鎖三種。

1. 重量級鎖會阻塞、喚醒請求加鎖的線程。它針對的是多個線程同時競爭同一把鎖的情況。JVM採用了自適應自旋,來避免線程在面對非常小的synchronized代碼塊時,仍會被阻塞、喚醒的情況。

2. 輕量級鎖採用CAS操作,將鎖對象的標記字段替換爲一個指針,指向當前線程棧上的一塊空間,存儲着鎖對象原本的標記字段。它針對的是多個線程在不同時間段申請同一把鎖的情況。

3. 偏向鎖只會在第一次請求時採用CAS操作,在鎖對象的標記字段中記錄下當前線程的地址。在之後的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況。

 

2、鎖粗化:當多次連續的加鎖與解鎖過程,會將多次加減鎖過程粗化爲一次加鎖與解鎖的過程

鎖粗化就是將多次連接在一起的加鎖、解鎖操作合併爲一次,將多個連續的鎖擴展成爲一個範圍更大的鎖。舉例如 下:

public class Test{

private static StringBuffer sb = new StringBuffer();

public static void main(String[] args) {

sb.append("a");

sb.append("b");

sb.append("c");

}

}

這裏每次調用stringBuffffer.append方法都需要加鎖和解鎖,如果虛擬機檢測到有一系列連串的對同一個對象加鎖

和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次

append方法結束後進行解鎖。

 

3、鎖消除:當對象不屬於共享資源時,對象內部的同步方法或同步代碼塊的鎖會自動解除

鎖消除即刪除不必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,

那麼可以認爲這段代碼是線程安全的,不必要加鎖。看下面這段程序:

public class Test{

public static void main(String[] args) {

StringBuffer sb = new StringBuffer();

sb.append("a").append("b").append("c");

}

}

雖然StringBuffffer的append是一個同步方法,但是這段程序中的StringBuffffer屬於一個局部變量,並且不會從該方

法中逃逸出去,所以其實這過程是線程安全的,可以將鎖消除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章