多線程:synchronized關鍵字解析

原理

synchronized是JVM層面的鎖,是一種重量級的鎖。synchronized可以同步方法和代碼塊。

public class Synchronized {
    public static void main(String[] args) {
    // 對Synchronized Class對象進行加鎖
        synchronized (Synchronized.class) {
        }
    // 靜態同步方法,對Synchronized Class對象進行加鎖
        m();
    }
    public static synchronized void m() {
    }
}

執行javap - v Synchronized

public static void main(java.lang.String[]);
// 方法修飾符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
    Code:
        stack=2, locals=1, args_size=1
        0: ldc #1  // class com/murdock/books/multithread/book/Synchronized
        2: dup
        3: monitorenter  // monitorenter:監視器進入,獲取鎖
        4: monitorexit   // monitorexit:監視器退出,釋放鎖
        5: invokestatic  #16 // Method m:()V
        8: return

    public static synchronized void m();
    // 方法修飾符,表示: public static synchronized
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
            stack=0, locals=0, args_size=0
            0: return

方法級別的同步是隱式的,無需通過字節碼指令來控制,它依靠的是方法表裏的ACC_SYNCHRONIZED標誌(什麼是方法表和標誌?),當方法調用時,調用指令會檢查方法的ACC_SYNCHRONIZED是否被設置了,如果被設置了執行線程首先需要持有管程才能執行方法,執行後或異常時釋放管程。

而代碼塊級別的同步依靠的是monitorenter和monitorexit指令,這兩個指令總是成對執行的,在程序異常時編譯器會生成一個異常處理器來執行monitorexit指令。

無論採用哪種方式,都是對一個對象的監視器或叫做管程(Monitor)進行獲取,這個過程是排他的,也就是同一時刻只可以有一個線程獲取到有synchronized保護對象的監視器。獲取不到的線程會阻塞在同步方法或同步塊的入口處,進入BLOCKED阻塞狀態。這裏要區別一下阻塞狀態和等待狀態,使用Object的wait方法後會進入等待隊列,notify後喚醒線程從等待隊列移入到阻塞(同步)隊列。線程正常結束或者異常釋放monitor。

以下是對象,對象的監視器,同步隊列以及執行線程的關係

另外,JVM對重量級鎖進行了優化,在對象頭裏存放着鎖的類型和偏向線程id。                                 

偏向鎖:某個線程用這個鎖用的比較頻繁,那就把這個線程id存起來,鎖類型設爲偏向鎖。那麼下次如果還是他來獲取鎖的話,不用CAS直接將鎖給他。

輕量級鎖:多個線程競爭同步資源時,沒有獲取資源的線程自旋等待鎖釋放。

鎖的級別從低到高爲:無狀態鎖,偏向鎖,輕量級鎖(自旋),重量級鎖。鎖只可以升級不可以降級。

使用

兩個線程操作同一個對象裏的實例變量,爲什麼是實例變量?因爲局部變量是沒有線程安全問題的。

不安全的代碼如下:

public class HasSelfPrivateNum {
     private  int num = 0;
     public void  addi(String username){    (1)
          try{
               if (username.equals("a")){
                    num = 100;
                    System.out.println("a set over!");
                    Thread.sleep(3000);
               }else {
                    num = 200;
                    System.out.println("b set over!");
               }
               System.out.println( username + " num = " + num);
          }catch (InterruptedException e){
               e.printStackTrace();
          }
     }
}
public class ThreadA extends Thread {
     private HasSelfPrivateNum num;
     public ThreadA(HasSelfPrivateNum num){
          this.num = num;
     }
     @Override
     public void run() {
          super.run();
          num.addi("a");
     }
}
public class ThreadB extends Thread{
     private HasSelfPrivateNum num;
     public ThreadB(HasSelfPrivateNum num){
          this.num = num;
     }
     @Override
     public void run() {
          super.run();
          num.addi("b");
     }
}
public class Run {
     public static void main(String[] args) {
          HasSelfPrivateNum num = new HasSelfPrivateNum();
          // HasSelfPrivateNum num1 = new HasSelfPrivateNum();    (2)
          ThreadA threadA = new ThreadA(num);
          threadA.start();
          ThreadB threadB = new ThreadB(num);    (3)
          threadB.start();
     }
}

執行結果:

a set over!
b set over!
b num = 200
a num = 200

執行結果顯然發生了線程安全的問題。

接下來:

使用synchronized同步方法,在HasSelfPrivateNum的方法(1)上添加sychronized,即

synchronized public void  addi(String username){...}

此時的執行結果爲:

a set over!
a num = 100
b set over!
b num = 200

接下來:

在之前添加synchronized的基礎上,我們將之前兩個線程訪問同一個對象改爲每個線程單獨訪問一個對象,將Run類中的(2)的註釋打開,將(3)處傳入的對象改爲num1。

此時的執行結果爲:

a set over!
b set over!
b num = 200
a num = 100

可以看到沒有線程安全問題,但是執行結果的順序是交叉的。

    這是因爲關鍵詞synchronized取得的都是對象的鎖,所以當兩個線程訪問同一個對象的時候,這個對象的鎖沒有釋放另一個線程就無法訪問,執行結果就會是按照順序的。但是如果兩個線程執行的是同一個類的兩個對象,那麼就會創建兩個鎖,兩個線程分別執行互不影響。所以執行結果就會是交叉的。

以上代碼證明了多個線程可以異步操作多個對象的同一個sychronized方法。

但是,多個線程卻不可以操作同一個類的同一個sychronized類型的靜態方法,因爲同步方法因爲可以有多個對象所以會對應多個monitor,而靜態方法只會對應一個monitor。多個線程訪問時只有一個可以獲取monitor。

接下來討論一下同步方法和同步代碼塊的區別,以實例的爲例。

代碼如下:

public class Var {

    synchronized public void methodA(){
        try {
            System.out.println(Thread.currentThread().getName() + " run method A " + System.currentTimeMillis());
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void methodB(){
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + " run method B " + System.currentTimeMillis());
        }
    }

    public void methodC(){
        String syn = "synchtronized";
        synchronized (syn){
            System.out.println(Thread.currentThread().getName() + " run method C " + System.currentTimeMillis());
        }
    }

}
public class Test {
    public static void main(String[] args) {
        Var var = new Var();

        new Thread(new Runnable() {
            @Override
            public void run() {
                var.methodA();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                var.methodB();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                var.methodC();
            }
        }).start();

    }
}

執行結果:

Thread-0 run method A 1520142974301
Thread-2 run method C 1520142974301
Thread-1 run method B 1520142977302

可以看到方法B比其兩個方法打印慢3秒,線程Threa-0首先獲得對象var的鎖,接着線程Thread-0會休眠3秒,這時雖然線程Thread-1先進入線程規劃器,但是因爲方法methodB內部使用了sychronized代碼塊,而因爲methodC同步的只是方法內部的一個變量所以可以執行。

髒讀

發生髒讀的代碼如下:

public class PublicVar {

    public String username = "A";
    public String password = "AA";
    synchronized public void setValue(String username,String password){
        try {
            this.username = username;
            Thread.sleep(1000);
            this.password = password;
            System.out.println("current thread = " + Thread.currentThread().getName() + " username = " + username
            + " password = " + password);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public void getValue(){
        System.out.println("username = " + username + " password = " + password);
    }

}
public class ThreadA extends Thread {

    private PublicVar publicVar;
    public ThreadA(PublicVar publicVar){
        super();
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        super.run();
        publicVar.setValue("B","BB");
    }
}
public class Test
{
    public static void main(String[] args) {
        try {
            PublicVar publicVar = new PublicVar();
            ThreadA threadA = new ThreadA(publicVar);
            threadA.start();
            Thread.sleep(500); // 打印結果受此值影響,大於線程threadA(即setValue方法)休眠的時間就不會出現髒讀
            publicVar.getValue();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

執行結果:

username = B password = AA
current thread = Thread-0 username = B password = BB

如果也將getValue設爲sychronized,那麼執行結果:

current thread = Thread-0 username = B password = BB
username = B password = BB

此實驗可以得到另個結論:

1,A線程先持有object對象的Lock鎖,B線程可以以異步的方式調用object的非sychronized;

2,A線程持持有object對象的Lock鎖,B線程如果要調用object的sychronized類型方法則需等待,也就是同步。

第一次執行的時候,線程threadA先獲得publicVar對象的鎖,但是main線程依然可以調用publicVar對象的非sychronized方法getValue,此時username已被更改,password沒被該。

第二次執行的時候,線程threadA先獲得publicVar對象的鎖,但是main線程在threadA沒有執行完成setValue方法之前是不可以調用publicVar對象的sychronized方法getValue的,也就是隻有threadA釋放了鎖,將username和password都賦值了,main線程纔可以獲取publicVar的鎖進而調用getValue方法。

爲什麼會這樣呢?之前提到過在調用方法前會檢查方法的ACC_SYNCHRONIZED標誌是否被標誌了,標誌的情況下才需要獲取鎖,如果沒有標誌即使這個對象的鎖沒有被當前對象持有依然可以執行。所以實例方法同步的是對象,靜態方法同步的是類這個說法不是很全面。

sychronized鎖重入

    sychronized關鍵字擁有鎖重入的功能,也就是在一個線程得到一個對象瑣時,再次請求此對象鎖時是可以得到對象鎖的,廣義的可重入鎖也叫遞歸鎖,是指同一線程外層函數獲得鎖之後,內層還可以再次獲得此鎖。這也證明了在sychronized方法內部調用本類的其他sychronized方法時,是可以永遠得到鎖的。

 

 

參考:《深入理解JVM虛擬機》《Java併發編程的藝術》

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