3. synchronized關鍵字

synchronized的出現打破了volatile關鍵字的侷限性(無法保證原子性和只能修飾單一變量),它可以用來鎖住代碼塊、實例對象、類對象。

synchronized的使用

a. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊前要獲得給定對象的鎖

//Thread類
public class MyThread implements Runnable{
    private static int count;

    public MyThread(){
        count = 0;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "準備開始執行!");
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + (count++));
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


//Main函數
public class Main {
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread, "thread1");
        Thread t2 = new Thread(myThread, "thread2");
        t1.start();
        t2.start();
    }
}

執行結果:

thread1準備開始執行!
thread1: 0
thread2準備開始執行!
thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread2: 5
thread2: 6
thread2: 7
thread2: 8
thread2: 9

從結果可以看出,在run方法中,thread2的自增輸出在thread1的自增輸出後,而沒有被修飾到的print語句則可以先執行。說明在synchronized修飾代碼塊時獲得這個對象的鎖且只鎖住這段代碼塊,不影響其他未被修飾部分的執行。

b.修飾實例方法,作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖

將synchronized用來修飾run方法。

public synchronized void run() {
    System.out.println(Thread.currentThread().getName() + "準備開始執行!");
    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + ": " + (count++));
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

再執行,得到的結果如下:

thread1準備開始執行!
thread1: 0
thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread2準備開始執行!
thread2: 5
thread2: 6
thread2: 7
thread2: 8
thread2: 9

從結果可以看出,方法被thread1搶佔後,整個方法被鎖住,thread2要等待thread1執行退出後才能執行。

但如果將傳入兩個不同實體,讓其在兩個線程跑,即修改main函數爲:

MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
Thread t1 = new Thread(thread1, "thread1");
Thread t2 = new Thread(thread2, "thread2");
t1.start();
t2.start();

再執行,得到結果:

thread1準備開始執行!
thread1: 0
thread2準備開始執行!
thread2: 1
thread1: 2
thread2: 3
thread1: 4
thread2: 5
thread2: 6
thread1: 6
thread1: 7
thread2: 7

由此可見,synchronized在修飾實例方法是,只是對實例對象進行了加鎖,不影響到其他實例對象鎖的獲取。

c. 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖 

再接着將MyThread類的count自增封裝爲一個靜態函數,並用synchronized修飾。即改爲:

private static synchronized void count(){
    System.out.println(Thread.currentThread().getName() + "準備開始執行!");
    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + ": " + (count++));
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@Override
public void run() {
    count();
}

再運行得到結果:

thread1準備開始執行!
thread1: 0
thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread2準備開始執行!
thread2: 5
thread2: 6
thread2: 7
thread2: 8
thread2: 9

可以看出,synchronized修飾靜態方法時,類的對象實例調用函數時是對類進行加鎖的,所以類的其他對象實例被掛起等待。

d. synchronized和volatile的區別

(1)volatile只能作用於變量,使用範圍較小。synchronized可以用在方法、類、同步代碼塊等,使用範圍比較廣。 (要說明的是,java裏不能直接使用synchronized聲明一個變量,而是使用synchronized去修飾一個代碼塊或一個方法或類。)
(2)volatile只能保證可見性和有序性,不能保證原子性。而可見性、有序性、原子性synchronized都可以保證。 
(3)volatile不會造成線程阻塞。synchronized可能會造成線程阻塞。

 

   synchronized修飾的部分被一個線程上鎖(重量鎖)的後,其他線程如果來訪問這部分代碼就會被阻塞,直到解鎖後纔會被喚醒執行。而java的線程是映射到操作系統原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統介入,需要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,因爲用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工作。爲了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,synchronized在JDK 1.6以後做了優化,引入了新的鎖機制。

四種鎖態

鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。

a.存儲方式

鎖的狀態保存在對象的頭文件中,以32位的JDK爲例:

          位數

鎖狀態

                                    25bit 4bit 1bit 2bit
23bit 2bit 是否偏向鎖 鎖標誌位
無鎖 對象的hashcode 分代年齡 0 01
偏向鎖 線程ID 偏向時間戳 分代年齡 1 01
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量的指針 10
GC標記 11

b.偏向鎖

偏向鎖目的是爲了減少數據在無競爭情況下的性能消耗。其核心思想就是鎖會偏向第一個獲取它的線程,在接下來的執行過程中該鎖沒有其他的線程獲取,則持有偏向鎖的線程永遠不需要再進行同步。

  1. 獲取:當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏儲存鎖偏向的線程ID。以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要檢查當前Mark Word中儲存的線程是否指向當前線程,如果成功,表示已經獲得對象鎖;如果檢測失敗,則需要再測試一下Mark Word中偏向鎖的標誌是否已經被置爲1(表示當前鎖是偏向鎖):如果沒有則使用CAS操作競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
  2. 釋放:偏向鎖使用一種等待競爭出現才釋放鎖的機制,所以當有其他線程嘗試獲得鎖時,纔會釋放鎖。偏向鎖的撤銷,需要等到安全點。它首先會暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果不處於活動狀態,則將對象頭設置爲無鎖狀態;如果依然活動,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向其他線程,要麼恢復到無鎖或者標記對象不合適作爲偏向鎖(膨脹爲輕量級鎖),最後喚醒暫停的線程。

c.輕量鎖

輕量鎖本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

  1. 獲取:線程在執行同步塊之前,JVM會現在當前線程的棧幀中創建用於儲存鎖記錄的空間(LockRecord),並將對象頭的Mark Word信息複製到鎖記錄中。然後線程嘗試使用CAS將對象頭的MarkWord替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,並且對象的鎖標誌位轉變爲“00”,如果失敗,表示其他線程競爭鎖,當前線程便會嘗試自旋獲取鎖。如果有兩條以上的線程競爭同一個鎖,那麼輕量級鎖就不再有效,要膨脹爲重量級鎖,鎖標誌的狀態變爲“10”,MarkWord中儲存的就是指向重量級鎖(互斥量)的指針,後面等待的線程也要進入阻塞狀態。
  2. 釋放:輕量級鎖解鎖時,同樣通過CAS操作將對象頭換回來。如果成功,則表示沒有競爭發生。如果失敗,說明有其他線程嘗試過獲取該鎖,鎖同樣會膨脹爲重量級鎖。在釋放鎖的同時,喚醒被掛起的線程。

d.狀態轉換圖

鎖對比

優點

缺點

適用場景

偏向鎖

加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

競爭的線程不會阻塞,提高了程序的響應速度。

如果始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度非常快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

其他優化

  1. 適應性自旋:當線程在獲取輕量級鎖的過程中執行CAS操作失敗時,是要通過自旋來獲取重量級鎖的。問題在於,自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其循環10次,如果還沒獲取到鎖就進入阻塞狀態。但是JDK採用了更聰明的方式——適應性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。
  2. 鎖粗化:鎖粗化的概念應該比較好理解,就是將多次連接在一起的加鎖、解鎖操作合併爲一次,將多個連續的鎖擴展成一個範圍更大的鎖。
  3. 鎖消除:鎖消除即刪除不必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼可以認爲這段代碼是線程安全的,不必要加鎖。

 

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