Java併發編程之synchronized關鍵字

1. synchronized基本用法

  • 定義:如果一個對象對多個線程可見,synchronized能夠保證在同一時刻最多隻有一個線程操作這個對象,以達到保證併發安全的效果。
  • 作用:保證可見性和原子性,可以避免線程安全問題:運行結果錯誤
  • 兩種使用方法:
    1. 對象鎖:

      1. 方法鎖,默認鎖對象爲this當前實例對象
      public class ObjectLock3 implements Runnable {
          @Override
          public void run() {
              method();
          }
      
          public synchronized void method() {
              System.out.println(Thread.currentThread().getName() + "進入同步方法");
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      
          public static void main(String[] args) {
              ObjectLock3 objectLock3 = new ObjectLock3();
              Thread t1 = new Thread(objectLock3);
              Thread t2 = new Thread(objectLock3);
              t1.start();
              t2.start();
          }
      }
      
      1. 同步代碼塊鎖,自己指定鎖對象
      public class ObjectLock1 implements Runnable {
      
          @Override
          public void run() {
              synchronized (this) {
                  System.out.println(Thread.currentThread().getName() + "進入同步代碼塊");
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代碼塊");
              }
          }
      
          public static void main(String[] args) {
              ObjectLock1 objectLock1 = new ObjectLock1();
              new Thread(objectLock1).start();
              new Thread(objectLock1).start();
          }
      }
      Thread-0進入同步代碼塊
      Thread-0退出同步代碼塊
      Thread-1進入同步代碼塊
      Thread-1退出同步代碼塊
      
      public class ObjectLock2 implements Runnable {
      
          private static final Object lock1 = new Object();
          private static final Object lock2 = new Object();
      
          @Override
          public void run() {
              synchronized (lock1) {
                  System.out.println(Thread.currentThread().getName() + "進入同步代碼塊1");
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代碼塊1");
              }
              synchronized (lock2) {
                  System.out.println(Thread.currentThread().getName() + "進入同步代碼塊2");
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代碼塊2");
              }
          }
          public static void main(String[] args) {
              ObjectLock2 objectLock2 = new ObjectLock2();
              new Thread(objectLock2).start();
              new Thread(objectLock2).start();
          }
      }
      Thread-0進入同步代碼塊1
      Thread-0退出同步代碼塊1
      Thread-0進入同步代碼塊2
      Thread-1進入同步代碼塊1
      Thread-1退出同步代碼塊1
      Thread-0退出同步代碼塊2
      Thread-1進入同步代碼塊2
      Thread-1退出同步代碼塊2
      
    2. 類鎖:

      1. 靜態方法鎖,synchronized加在static方法上,鎖對象爲當前類
      public class ObjectStaticLock1 implements Runnable {
      
          @Override
          public void run() {
              method();
          }
      
          public static synchronized void method() {
              System.out.println(Thread.currentThread().getName() + "進入到同步靜態方法中");
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + "退出同步靜態方法");
          }
      
          public static void main(String[] args) {
              ObjectStaticLock1 objectStaticLock1 = new ObjectStaticLock1();
              ObjectStaticLock1 objectStaticLock2 = new ObjectStaticLock1();
              Thread t1 = new Thread(objectStaticLock1);
              Thread t2 = new Thread(objectStaticLock2);
              t1.start();
              t2.start();
          }
      }
      
      1. 同步代碼塊鎖,synchronized(*.class)代碼塊,指定鎖對象爲class對象,所謂的類鎖,不過是Class對象的鎖而已
      public class ObjectStaticLock2 implements Runnable {
          @Override
          public void run() {
              synchronized (ObjectStaticLock2.class) {
                  System.out.println(Thread.currentThread().getName() + "進入到同步代碼塊");
                  try {
                      Thread.sleep(2000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "退出同步代碼塊");
              }
          }
      
          public static void main(String[] args) {
              ObjectStaticLock2 objectStaticLock1 = new ObjectStaticLock2();
              ObjectStaticLock2 objectStaticLock2 = new ObjectStaticLock2();
              Thread t1 = new Thread(objectStaticLock1);
              Thread t2 = new Thread(objectStaticLock2);
              t1.start();
              t2.start();
          }
      }
      

2. 多線程訪問同步方法的7種情況

  1. 兩個線程同時訪問一個對象的同步方法:會發生同步,鎖對象都爲同一個實例對象;
  2. 兩個線程同時訪問兩個對象的同步方法:互不影響,鎖對象不同;
  3. 兩個線程訪問的是synchronized的靜態方法:會發生同步,鎖對象都爲Class對象,Class對象只有一個;
  4. 同時訪問同步方法和非同步方法:非同步方法不受影響,不發生同步;
  5. 訪問同一個對象不同的普通同步方法:會發生同步,鎖對象默認爲同一個實例對象;
  6. 同時訪問靜態synchronized和非靜態synchronized方法:互不影響,靜態syn方法的鎖對象爲Class對象,非靜態syn方法的鎖對象爲一個實例對象this,實例對象和Class對象不是同一個對象,實例對象在堆中,Class對象在方法區中;
  7. 方法拋出異常後,會釋放鎖

總結:
1. 一把鎖只能同時被一個線程獲取,沒拿到鎖的線程必須等待,如1、5;
2. 每個實例都有自己的一把鎖,不同實例互不影響,當使用Class對象以及synchonized修飾的static方法的時候,所有對象共用同一把類鎖,對應2、3、4、5;
3. 遇到異常,會釋放鎖,對應7;

3. synchronized關鍵字的性質

3.1 可重入

  • 定義:一個線程已經獲取到鎖,想再次獲取到這把鎖時不需要釋放,直接可以用;
  • 什麼是不可重入:一個線程獲取到鎖之後,想再次使用這個鎖,必須釋放鎖之後還其他線程競爭;
  • 好處:避免死鎖:假如一個類有兩個synchronized方法,當一個線程執行了方法1獲得了默認的this對象鎖,這個時候要執行方法2,如果synchronized不具備可重入性,那麼這個線程就無法獲取到訪問方法2的鎖,又無法釋放鎖,就造成了死鎖。
  • 粒度:線程範圍,在一個線程中,只要這個線程拿到了這把鎖,在這個線程內部就可以一直使用
    1. 同一個方法是可重入的;
    2. 可重入不要求是同一個方法;
    3. 可重入不要求是同一個類中;

3.2 不可中斷

一旦這個鎖已經被別的線程獲得了,如果本線程還想獲得,該線程只能等待或阻塞,直到別的線程釋放這個鎖。如果別的線程永遠不釋放鎖,那麼本線程則永遠等待下去。
相比之下,Lock類,擁有可以中斷的能力:

  • 如果等的時間過長,可以中斷現在已經獲取的鎖的線程的執行;
  • 如果等待時間過長,也可以退出。

4. synchronized原理

4.1 加鎖和釋放鎖原理

  • 每個一個對象都有一個內置的monitor鎖,這個鎖存儲在對象頭中的,鎖的獲取和釋放實際上需要執行兩個指令:monitorenter和monitorexit,當線程執行到monitorenter的時候會嘗試獲取這個鎖;
  • 反編譯:先javac demo.java,然後javap -verbose demo.class文件;
  • monitorenter和monitorexit在執行的時候會讓對象鎖的計數+1或-1;
  • 獲取鎖的過程:首先一個線程要獲取一個對象鎖的時候會查看這個monitor鎖的計數器如果爲0,那麼就給他+1,這樣別的線程就進不來了,如果一個線程有了這把鎖,又重入了,在計數器再+1;如果monitor被其他線程持有了,直到計數器=0,纔會獲取這個鎖。
  • 釋放鎖的過程:將monitor的計數器-1,直到=0,表示不再擁有所有權了,如果不是0,說明剛纔是可重入進來的

4.2 可重入原理

一個線程拿到一把鎖之後,還想再次進入由這把鎖所控制的方法,則可以再次進入,原理是用了monitor鎖的計數器。

  • JVM負責跟蹤被加鎖的次數
  • 線程第一次給對象加鎖的時候,計數+1.每當這個相同的線程再次獲取該對象鎖的時候,計數器會遞增;
  • 每當任務離開的時候,計數遞減,當計數爲0的時候,鎖被完全釋放;

4.3 可見性原理

線程A和線程B通信:

  1. 本地內存A把修改後的內容放到主內存中;
  2. 本地內存B從主內存從讀取修改後的內容;

synchnized修飾的代碼塊對對象的任何修改,在釋放鎖之前都要將修改的內容先寫回到主內存中,所以從主內存中讀取的內容都是最新的。

5. synchronized的缺陷

  • 效率低:鎖的釋放情況少(只有代碼執行完和拋異常)、試圖獲得鎖時候不能設定超時、不能中斷一個正在試圖獲得鎖的線程
  • 不夠靈活:加鎖和釋放的時機單一,每個鎖僅僅有單一的條件,可能是不夠的。讀寫鎖更靈活。
  • 無法知道是否成功獲取到鎖,沒法去嘗試獲取,去判斷。Lock是可以通過tryLock方法嘗試獲取,返回true代表成功加鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章