1. 程式人生 > >多執行緒:synchronized關鍵字解析

多執行緒: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併發程式設計的藝術》