1. 程式人生 > >synchronized原理及其相關特性

synchronized原理及其相關特性

1.synchronized

synchronized:可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為互斥區或臨界區。

synchronized可以保證方法或者程式碼塊在執行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變數的記憶體可見性。

採用synchronized修飾符實現的同步機制叫做互斥鎖機制,它所獲得的鎖叫做互斥鎖。每個物件都有一個monitor(鎖標記),當執行緒擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池。任何一個物件系統都會為其建立一個互斥鎖,這個鎖是為了分配給執行緒的,防止打斷原子操作。每個物件的鎖只能分配給一個執行緒,因此叫做互斥鎖。

 

Java中每一個物件都可以作為鎖,這是synchronized實現同步的基礎:

  1. 普通同步方法,鎖是當前例項物件

  2. 靜態同步方法,鎖是當前類的class物件

  3. 同步方法塊,鎖是括號裡面的物件

其實synchronized相當於是一個“物件監控器”鎖,這個物件監控器由不同的物件充當。

當一個執行緒訪問同步程式碼塊時,它首先是需要得到鎖才能執行同步程式碼,當退出或者丟擲異常時必須要釋放鎖。

public class MyThread extends Thread {
    private int count = 5;
    @Override
    public synchronized void run() {
        count‐‐;
        System.out.println(this.currentThread().getName() + " count:" + count);
} 
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread, "thread1");
        Thread thread2 = new Thread(myThread, "thread2");
        Thread thread3 = new Thread(myThread, "thread3");
        Thread thread4 = new Thread(myThread, "thread4");
        Thread thread5 = new Thread(myThread, "thread5");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}

輸出結果:

thread1 count:4
thread2 count:3
thread3 count:2
thread5 count:1
thread4 count:0

這裡在run方法上加了synchronized這個修飾。主要作用就是當多個執行緒訪問MyThread的run方法的時候,他們就會以排隊的形式(這裡排隊是按照CPU分配的先後順序而定的)執行,一個執行緒如果想執行synchronized修飾的方法裡的程式碼,就要先獲得這個物件的鎖,如果獲取不到,就不能執行,會一直嘗試在獲得這個鎖。

synchronized同步程式碼塊 

使用關鍵字synchronized修飾同步方法是有弊端的,比如當執行緒A要執行一個比較長時間的同步方法的時候,這個時候執行緒B就必須要等待比較長的時間。這個時候就可以使用synchronized同步程式碼塊來解決問題。

當兩個併發執行緒訪問同一物件object中的synchronized(this)同步程式碼塊時,一段時間內只能有一個執行緒被執行,另外一個執行緒必須等待當前執行緒執行完這個程式碼塊才可以執行改程式碼塊。其實這個有點類似這篇部落格中的ReentrantLock分組列印。

所以synchronized同步程式碼塊存在著上面所說的一個問題,就是一次只能執行一個程式碼塊,如果有多個synchronized同步程式碼塊的時候,就會陷入阻塞狀態,這樣子會影響效率,這個時候我們可以修改synchronized程式碼塊中的監控物件那個,即使用同步程式碼塊鎖非this物件的時候,synchronized(非this)程式碼塊中的程式與同步方法是非同步的,不與其他thi鎖this同步方法爭搶this鎖,可以提高執行效率。

synchronized同步程式碼塊還可以是任意物件,比如:

try{
    String x=new String();
    synchronized(x){
        ...
    }
}

當呼叫同步程式碼塊的時候,每次都是一個新的同步程式碼塊物件,這樣的話就不會出現分組列印,而是變成了交叉列印了。

這裡1,3其實是一樣的,就是一個分組列印的效果。2 的話就是當程式中即有同步方法又有同步程式碼塊的時候,兩者會是同步的,即呼叫同步方法或者同步程式碼塊都能實現分組效果。

synchronized靜態同步方法

鎖的是Class物件,跟synchronized同步方法不是同一個鎖,所以是非同步。但是對於所有的Class例項是同步的。比如線上程中建立一個Class例項,當他呼叫synchronized靜態同步方法的時候,實現了同步。

  • 一個物件有一把鎖,多個執行緒多個鎖

  • public class MultiThread {
        private int num = 200;
        public synchronized void printNum(String threadName, String tag) {
            if (tag.equals("a")) {
                num = num ‐ 100;
                System.out.println(threadName + " tag a,set num over!");
            } else {
                num = num ‐ 200;
                System.out.println(threadName + " tag b,set num over!");
            } 
            System.out.println(threadName + " tag " + tag + ", num = " + num);
    } 
        public static void main(String[] args) throws InterruptedException {
            final MultiThread multiThread1 = new MultiThread();
            final MultiThread multiThread2 = new MultiThread();
            new Thread(new Runnable() {
                public void run() {
                    multiThread1.printNum("thread1", "a");
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                multiThread2.printNum("thread2", "b");
            }
           }).start();
       }
    }

    此外,說明一下這裡的new Thread(new Runnable()),其實上是呼叫了Thread類的帶引數的構造法方法。

  • 輸出結果:

    thread1 tag a,set num over!
    thread1 tag a, num = 100
    thread2 tag b,set num over!
    thread2 tag b, num = 0

    這與我們期望的輸出結果:thread2 tag b, num = ‐100不大一樣,這是因為上面有兩個物件:multiThread1 和 multiThread2,他們使用了同一把鎖,所以就會出現這種情況。因為這裡synchronized鎖的是普通方法,所以鎖是當前例項物件。可以在變數和方法上加上static關鍵字,就可以實現我們想要的結果。因為加上了static,實際上鎖是變成了class物件了。

  • 物件鎖的同步和非同步

    • 同步:synchronized,共享資源,即保證了執行緒安全中的原子性,此外,執行緒安全還需要保證可見性,這個就需要volatitle實現

    • 非同步:asynchronized,多個執行緒之間不會競爭共享資源。

2.synchronized的特性

  • synchronized擁有可重入鎖

synchronized擁有鎖重入的功能,當一個執行緒得到一個物件的鎖後,在該鎖裡執行程式碼的時候可以再次獲得該物件的其他鎖。當執行緒請求一個由其它執行緒持有的物件鎖時,該執行緒會阻塞,而當執行緒請求由自己持有的物件鎖時,如果該鎖是重入鎖,請求就會成功,否則阻塞。

如下面這個例子中,同步方法可以呼叫自己內部的其他同步方法,即使還沒有釋放自己的同步鎖,還是可以獲得其他重入鎖。

public class SyncDubbo {

    public synchronized void method1() {
        System.out.println("method1-----");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2-----");
        method3();
    }

    public synchronized void method3() {
        System.out.println("method3-----");
    }

    public static void main(String[] args) {
        final SyncDubbo syncDubbo = new SyncDubbo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                syncDubbo.method1();
            }
        }).start();
    }
}

//執行結果:
method1-----
method2-----
method3-----

可重入鎖就是自己獲得自己內部的鎖。如果沒有重入鎖的話,加入有一個執行緒在獲取了物件A的鎖之後,再次請求A的鎖的時候,由於還沒有釋放之前獲得的鎖,所以這個時候就會出現死鎖。

假如有一個場景:使用者名稱和密碼儲存在本地txt檔案中,則登入驗證方法和更新密碼方法都應該被加synchronized,那麼當更新密碼的時候需要驗證密碼的合法性,所以需要呼叫驗證方法,此時是可以呼叫的。

此外,可重入鎖的特性還有父子可繼承性,如下面的例子:

public class SyncDubbo {

    static class Main {
        public int i = 5;
        public synchronized void operationSup() {
            i--;
            System.out.println("Main print i =" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Sub extends Main {
        public synchronized void operationSub() {
            while (i > 0) {
                i--;
                System.out.println("Sub print i = " + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                Sub sub = new Sub();
                sub.operationSub();
            }
        }).start();
    }
}

返回結果是:

Sub print i = 4
Sub print i = 3
Sub print i = 2
Sub print i = 1
Sub print i = 0
  • 其他特性

    • 出現異常時,鎖自動釋放,即一個執行緒的程式碼執行過程中出現異常的話,其所持有的鎖會自動釋放。

    • 將任意物件作為監視器monitor

public class StringLock {
    private String lock = "lock";
    public void method() {
        synchronized (lock) {
            try {
                    System.out.println("當前執行緒: " +
                    Thread.currentThread().getName() + "開始");
                    Thread.sleep(1000);
                    System.out.println("當前執行緒: " +Thread.currentThread().getName() +                     "結束");
} catch (InterruptedException e) {
            }
        }
    } 
    public static void main(String[] args) {
        final StringLock stringLock = new StringLock();
            new Thread(new Runnable() {
                public void run() {
                    stringLock.method();
                    }
            }, "t1").start();
            new Thread(new Runnable() {
                public void run() {
                    stringLock.method();
                    }
            }, "t2").start();
       }
}

執行結果:

當前執行緒: t1開始
當前執行緒: t1結束
當前執行緒: t2開始
當前執行緒: t2結束
  • 單例模式:­雙重校驗鎖

    • 普通加鎖的單例模式實現:
      public class Singleton {
          private static Singleton instance = null; //懶漢模式
          //private static Singleton instance = new Singleton(); //餓漢模式
          private Singleton() {
          } 
          public static synchronized Singleton newInstance() {
              if (null == instance) {
                  instance = new Singleton();
          } 
              return instance;
          }
      }

      使用上述的方式可以實現多執行緒的情況下獲取到正確的例項物件,但是每次訪問new Instance() 方法都會進行加鎖和解鎖操作,也就是說該鎖可能會成為系統的瓶頸。

    • .雙重校驗鎖:
      public class DubbleSingleton {
          private static volatile DubbleSingleton instance;
          public static DubbleSingleton getInstance(){
              if(instance == null){
                  try {
                      //模擬初始化物件的準備時間...
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
              } //類上加鎖,表示當前物件不可以在其他執行緒的時候建立
                  synchronized (DubbleSingleton.class) {
              //如果不加這一層判斷的話,這樣的話每一個執行緒會得到一個例項
             //而不是所有的執行緒的到的是一個例項
                  if(instance == null){
                      instance = new DubbleSingleton();
                  }
              }
          } 
          return instance;
          }
      }

      需要注意的是,如果沒有加上volatile這個關鍵字的話是錯誤的。因為指令重排優化,可能會導致初始化單例物件和將該物件地址賦值給instance欄位的順序與上面Java程式碼中書寫的順序不同。volatile關鍵字在這裡的含義就是禁止指令的重排序優化(另一個作用是提供記憶體可見性),從而保證instance欄位被初始化時,單例物件已經被完全初始化

volatile關鍵字的作用就是強制從公共堆疊中取得變數的值,而不是執行緒私有的資料棧中取得變數的值。