Java 併發編程(二)Synchronized原理剖析及使用

Java 併發編程之Synchronized原理剖析及使用

在開始介紹Synchronize之前,先了解一下在併發中極其重要的三個概念:原子性,可見行和有序性

  • 原子性: 是指一個操作不可以被中斷.比如賦值操作a=1和返回操作return a,這樣的操作在JVM中只需要一步就可以完成,因此具有原子性,而想自增操作a++這樣的操作就不具備原子性,a++在JVM中要一般經歷三個步驟:
    1. 從內存中取出a.
    2. 計算a+1.
    3. 將計算結果寫回內存中去.
  • 可見性: 一個線程對於共享變量的修改,能夠及時地被其他線程看到.
  • 有序性: 程序執行的順序按照代碼的先後邏輯順序執行.

只有同時保證了這三個特性才能認爲操作是線程安全的.
對於Java來說, 被關鍵字Synchronized修飾的同步代碼塊或者同步方法能保證每個時刻只有一個線程執行該同步代碼,自然便保證了有序性.在Java內存模型中,synchronized規定,在工作線程獲得所要加鎖的對象的鎖後:

  1. 獲得對象的互斥鎖
  2. 清空工作內存
  3. 在主內存中拷貝最新變量的副本到工作內存
  4. 執行同步代碼塊
  5. 將更改後的共享變量的值刷新到主內存中
  6. 釋放互斥鎖

其中步驟3與步驟5保證了操作的可見性,而Java內存模型保證從獲取互斥鎖到釋放互斥鎖的整個過程不會被其他線程中斷,因此也保證了操作的原子性.
接下來開始剖析Synchronized關鍵字的原理

Synchronized原理剖析


首先通過反編譯下面的代碼來看看JVM中Synchronized是如何實現對代碼塊進行同步的.

package com.paddx.test.concurrent; 
public class SynchronizedDemo { 
    public void method() { 
        synchronized (this) { 
             System.out.println("Method 1 start"); 
        } 
    } 
 } 

反編譯結果:

所以monitorentermonitorexit這兩個字節碼指令是個什麼鬼呢?來看看JVM規範中的描述:

monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

大概意思:
每個對象都有一個監視器鎖(monitor),一個monitor在每個時刻最多隻能被一個線程擁有,線程執行monitorenter字節碼指令時嘗試獲取該對象的監視器鎖(monitor)的所有權,獲取步驟如下:

  1. 如果monitor的進入數爲0,則該線程進入monitor,然後將進入數設置爲1,該線程即爲monitor的所有者
  2. 如果本線程已經擁有該monitor的所有權,只是重新進入,則monitor的進入數加1.
  3. 如果其他線程已經佔用了monitor,則本線程進入阻塞狀態(Synchronize在改進後是線程先進入自旋狀態等待,等待持有該對象鎖的線程釋放鎖就可立即獲得鎖,如果超過自旋等待的最大時間仍未能獲得該對象鎖,則本線程停止自旋進入阻塞狀態),直到monitor的進入數爲0,再重新嘗試獲取monitor的所有權.

monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

大概意思:
執行字節碼指令monitorexit的線程必須是擁有該監視器(monitor)的所有者.
執行該命令後,monitor的進入數-1,當monitor的進入數變爲0時,該線程就失去了該監視器的所有權,即釋放該對象的監視器鎖,其它被該monitor阻塞的線程可以開始嘗試獲取該對象的監視器鎖.

JVM基於進入和退出Monitor對象來實現代碼塊同步和方法同步,兩者實現形式不同,但是實現原理無本質區別.
經過上面的分析可知同步代碼塊使用monitorentermonitorexit字節碼指令實現,而方法同步是在其常量池中多了ACC_SYNCHRONIZED標誌符號.

JVM實現同步方法原理:當方法被調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor.
在方法執行期間,其他任何線程都無法再獲得同一個monitor對象.

Synchronized的使用


總體上,Synchronized一般有三種用法:

  • 修飾代碼塊
  • 修飾普通方法
  • 修飾靜態方法
  1. 同步一個代碼塊
public void func() {
    synchronized (this) {
        // ...
    }
}

this指的是當前類的對象的引用,使用synchronized修飾this的意思就是嘗試獲取當前類的對象的監視器鎖(monitor),當本線程擁有該對象監視器鎖時,其他嘗試獲取該對象的監視器鎖的線程自然會被阻塞.如果其他線程調用的是同一個類的不同對象上的同步代碼塊,就不會被阻塞.

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}

代碼運行結果:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

調用同一個類的不同對象的同步代碼塊,不會出現同步.

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}

代碼運行結果:(如果沒有出現該不同步的現象,主要是現在cpu運行速度太快了,將數值設置大一些即可)

0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
  1. 同步一個普通方法
public synchronized void func () {
    // ...
}

與同步代碼塊一樣,只作用於同一個對象,不再贅述.

  1. 同步一個靜態方法
public synchronized static void fun() {
    // ...
}

作用於整個類.在同一時刻該類的同步方法只能被一個線程調用.
4. 同步一個類

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}

代碼運行結果:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

作用於整個類,兩個線程調用同一個類的不同對象上的這種同步語句,也會進行同步。

學習Synchronized關鍵字遇到的一個小困惑

廢話不多說,直接上代碼

class SynchronizedExample{
    private int value;
    public synchronized void setValue(int value){
       this.value=value;
    }
    public int getValue(){
        return value;
    }
}

使用多個線程共享value值,假如某個線程調用了setValue()方法,另一個線程看到的並不一定是更新後的value值.這裏就涉及了內存可見性問題.getValue()方法並不是同步方法,調用getValue()不需要持有對象鎖,
爲了確保所有線程能夠看到共享變量的最新值,可以在所有執行讀操作和寫操作的線程上加上同一把對象鎖.這樣讀操作,就可以看到最新的寫操作之前所有的共享變量的狀態變化了.

當線程 A 執行某個同步代碼塊時,線程 B 隨後進入由同一個鎖保護的同步代碼塊,這種情況下可以保證,當鎖被釋放前,A 看到的所有變量值(鎖釋放前,A 看到的變量包括 y 和 x)在 B 獲得同一個鎖後同樣可以由 B 看到。換句話說,當線程 B 執行由鎖保護的同步代碼塊時,可以看到線程 A 之前在同一個鎖保護的同步代碼塊中的所有操作結果。如果在線程 A unlock M 之後,線程 B 才進入 lock M,那麼線程 B 都可以看到線程 A unlock M 之前的操作,可以得到 i=1,j=1。如果在線程 B unlock M 之後,線程 A 才進入 lock M,那麼線程 B 就不一定能看到線程 A 中的操作,因此 j 的值就不一定是 1。

class SynchronizedExample{
    private int value;
    public synchronized void setValue(int value){
       this.value=value;
    }
    public synchronized int getValue(){
        return value;
    }
}

setValue()方法和getValue()方法都採用Synchronized關鍵字修飾,變成同步方法,即不同線程在調用同一個對象中的這兩個方法時需要獲得同一把鎖,這樣每次通過getValue()方法獲得的value值都是最新的值.

Java 併發編程(一)Volatile原理剖析及使用
Java 併發編程(二)Synchronized原理剖析及使用
Java 併發編程(三)Synchronized底層優化(偏向鎖與輕量級鎖)
Java 併發編程(四)JVM中鎖的優化
Java 併發編程(五)原子操作類

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