1. 程式人生 > >同步(一)

同步(一)

一、同步的基本概念

1、同步的場景
在這裡插入圖片描述
執行緒獲取同步鎖,獲取失敗則阻塞等待,獲取成功則執行任務,執行完畢後釋放鎖。

2、執行緒安全問題

(1)記憶體讀取

  • cpu在記憶體讀取資料時,順序優先順序:暫存器-快取記憶體-記憶體
  • 計算完成後快取資料寫回記憶體中

(2)可見性

  • 每個執行緒都有獨立的工作記憶體,並對其他執行緒是不可見的。執行緒執行如用到某變數,將變數從主記憶體複製到工作記憶體,對變數操作完成後再將變數寫回主記憶體。
  • 可見性就是指執行緒A對某變數操作後,其他執行緒能夠看到被修改的值,可以通過volidate等方式實現可見性。

(3)原子性

對於基本型別的賦值操作是原子操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。

x = 10;        //語句1(原子性操作)
y = x;         //語句2
x++;           //語句3
  • 語句1直接將10賦值給x,會直接將10寫入到工作記憶體
  • 語句2需要先讀x,然後將x寫入工作記憶體,然後賦值,不是原子性操作
  • 語句3需要先讀x,然後+1,然後賦值。

(4)有序性

  • Java記憶體模型中允許編譯器和處理器對指令進行重排序,雖然重排序過程不會影響到單執行緒執行的正確性,但是會影響到多執行緒併發執行的正確性。
  • 可以通過volidate來保證有序性,synchronized和Lock保證每個時刻只有一個執行緒執行同步程式碼,這相當於是讓執行緒順序執行同步程式碼,從而保證了有序性。

(5)執行緒安全問題的原因
在這裡插入圖片描述
執行緒A和執行緒B需要將共享變數拷貝到本地記憶體中,並對各自的共享變數副本進行操作,操作完成後同步到主記憶體中,可能導致共享記憶體資料錯亂的問題。

二、synchronized

1、基本特性:

  • synchronized可以用於修飾類的例項方法、靜態方法和程式碼塊
  • 可重入性:對同一個執行執行緒,它在獲得了鎖之後,在呼叫其他需要同樣鎖的程式碼時,可以直接呼叫。
  • 記憶體可見性:在釋放鎖時,所有寫入都會寫回記憶體,而獲得鎖後,都會從記憶體中讀最新資料。
  • 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的非synchronized(this)同步程式碼塊。

2、類鎖和物件鎖

public class SyncTest {
  private static int num;
    
  public void setClassText() {
     //類鎖
     synchronized (SyncTest.class) {
       System.out.println("類鎖開始執行");
       ++num;
       try {
         Thread.sleep(5000);
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
       System.out.println("類鎖執行完成");
     }
  }
  
  public static synchronized void setObject1Text() {
      //類鎖
      System.out.println("類鎖2開始執行");
      ++num;
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("類鎖2執行完成");
  }

  public synchronized void setObject1Text() {
      //物件鎖
      System.out.println("物件鎖1開始執行");
      ++num;
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("物件鎖1執行完成");
  }

  public void setObject2Text() {
    //物件鎖
    synchronized(this) {
      System.out.println("物件鎖2開始執行");
      ++num;
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("物件鎖2執行完成");
    }

  }
}

注:

  • 執行緒A持有物件鎖,不會影響到執行緒B去持有類鎖
  • 執行緒A和B操作同一個物件鎖,執行緒A持有物件鎖,執行緒B只能等到該物件鎖釋放
  • synchronized如果修飾static方法(類鎖),不會影響到非static(物件鎖)修飾的方法的呼叫

3、wait、notify

  • void notifyAl1( )
    解除那些在該物件上呼叫 wait 方法的執行緒的阻塞狀態該方法只能在同步方法或同步塊內部呼叫。 如果當前執行緒不是物件鎖的持有者,該方法丟擲一個IllegalMonitorStateException異常。
  • void notify()
    隨機選擇一個在該物件上呼叫 wait 方法的執行緒,解除其阻塞狀態。 該方法只能在一個同步方法或同步塊中呼叫。 如果當前執行緒不是物件鎖的持有者, 該方法丟擲一個 IllegalMonitorStateException 異常。
  • void wait(long mi11is)
    導致執行緒進人等待狀態直到它被通知。該方法只能在一個同步方法中呼叫。如果當前執行緒不是物件鎖的持有者,該方法丟擲一個 IllegalMonitorStateException 異常。
  • void wait(long mi11Is, int nanos)
    導致執行緒進入等待狀態直到它被通知或者經過指定的時間。 這些方法只能在一個同步方法中呼叫。如果當前執行緒不是物件鎖的持有者該方法丟擲一個 IllegalMonitorStateException 異常。

注意:

  • 呼叫notify會把在條件佇列中等待的執行緒喚醒並從佇列中移除,但它不會釋放物件鎖,只有在包含notify的synchronzied程式碼塊執行完後,等待的執行緒才會從wait呼叫中返回。
  • 呼叫wait把當前執行緒放入條件等待佇列,釋放物件鎖。等待時間到或被其他執行緒呼叫notify/notifyAll從條件佇列中移除,此時要重新競爭物件鎖。

三、鎖物件

鎖可以理解為進入某個門的鑰匙,兩個人都想進入門內,A持有鑰匙可進入,B只能等待。A放下了鑰匙,B才有機會獲取到鑰匙進入。

  • 鎖用於保護程式碼片段,只有一個執行緒執行被保護的程式碼
  • 需要保證是同一個鎖物件
  • 需要通過呼叫unlock去釋放鎖
  • 鎖可以擁有一個或多個相關的條件物件

1、Lock

public interface Lock {

  /**
   * 獲取鎖,如果鎖被另一執行緒擁有則發生阻塞
   */
  void lock();

  /**
   * 嘗試獲取鎖,立即返回不阻塞
   * @return 獲取成功返回true
   */
  boolean tryLock();
  
  /**
   * 嘗試獲取鎖,如果成功立即返回,否則阻塞等待,阻塞時間不會超    * 過給定的值
   * @return 獲取成功返回true
   */  
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

  /**
   * 釋放鎖
   */
  void unlock();

  /**
   * 獲取一個與該鎖相關的條件
   * @return
   */
  Condition newCondition();

    ......
}

synchronized和Lock對比:

  • 鎖可以以非阻塞方式獲取鎖、可以響應中斷、可以限時
  • synchronized會自動釋放鎖,而Lock一定要求程式設計師手工釋放

2、ReentrantLock:

(1)構造:

public ReentrantLock() {
  sync = new NonfairSync();
}

/**
 * 構建一個公平策略的鎖,公平鎖偏愛等待時候最長的執行緒,會降低效能
 * @param fair
 */
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

(2)條件物件

public Condition newCondition() {
......
}

一個鎖物件可以有一個或多個條件物件,通過newCondition獲取一個條件物件

(3)await、signalAll

  • void await( )
    將該執行緒放到條件的等待集中。
  • void signalAll( )
    解除該條件的等待集中的所有執行緒的阻塞狀態。
  • void signal()
    從該條件的等待集中隨機地選擇一個執行緒, 解除其阻塞狀態。

注意:

  • await()對應於Object的wait(),signal()對應於notify,signalAll()對應於notifyAll()
  • 呼叫await、signalAll等方法前需要先獲取鎖,如果沒有鎖,會丟擲異常IllegalMonitorStateException

3、讀寫鎖:

特性:

只要沒有任何執行緒寫入變數,併發讀取可變變數通常是安全的。所以讀鎖可以同時被多個執行緒持有,只要沒有執行緒持有寫鎖。這樣可以提升效能和吞吐量,因為讀取比寫入更加頻繁。

使用場景:多執行緒操作同一檔案,讀操作比較多,寫操作比較少的情況。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//獲取讀、寫鎖
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

//對讀的操作使用讀鎖
readLock.lock();
try {
  ......
} finally {
  readLock.unlock();
}

//對寫的操作使用寫鎖
writeLock.lock();
try {
  ......
} finally {
  writeLock.unlock();
}

四、Volidate域

1、應用場景:

  • 如果宣告一個域為 volatile, 它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
  • 對變數的寫操作不會依賴於當前值。(不保證原子性)
  • 該變數沒有包含在具有其他變數的不變式中

特性:

  • 保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
  • 禁止進行指令重排序。
  • Volatile變數不能提供原子性

例如

public class VolatileTest {

  volatile boolean flag;

  public void finish() {
    flag = true;
  }

  public void begin() {
    flag =false;
  }
}

通過volatile修飾保證標識位在多執行緒中的可見性

2、原理:

  • 它確保指令重排序時不會把其後面的指令排到被volidate修飾的變數之前,也不會把前面的指令排到該變數的後面;
  • 它會強制將對快取的修改操作立即寫入主存;
  • 如果是寫操作,它會導致其他CPU中對應的快取行無效。

3、synchronized和volatile比較:

  • volatile是執行緒同步的輕量級實現,並且volatile只能修飾於變數,而synchronized可以修飾方法,以及程式碼塊。
  • 多執行緒訪問volatile不會發生阻塞,而synchronized會出現阻塞
  • volatile能保證資料的可見性,但不能保證原子性;而synchronized可以保證原子性

五、死鎖

有a, b兩個執行緒,a持有鎖A,在等待鎖B,而b持有鎖B,在等待鎖A,a,b陷入了互相等待,最後誰都執行不下去。

public class DeadLockDemo {
  private static Object lockA = new Object();
  private static Object lockB = new Object();
  private static void startThreadA() {
    Thread aThread = new Thread() {
      @Override
      public void run() {
        synchronized (lockA) {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          synchronized (lockB) {
          }
        }
      }
    };
    aThread.start();
  }
  private static void startThreadB() {
    Thread bThread = new Thread() {
      @Override
      public void run() {
        synchronized (lockB) {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          synchronized (lockA) {
          }
        }
      }
    };
    bThread.start();
  }
  public static void main(String[] args) {
    startThreadA();
    startThreadB();
  }
}

解決方式:

  • 應該儘量避免在持有一個鎖的同時去申請另一個鎖,如果確實需要多個鎖,所有程式碼都應該按照相同的順序去申請鎖
  • 顯式鎖介面Lock,它支援嘗試獲取鎖(tryLock)和帶時間限制的獲取鎖方法,可以在獲取不到鎖的時候釋放已經持有的鎖,然後再次嘗試獲取鎖或乾脆放棄,以避免死鎖。