1. 程式人生 > 其它 >JUC之Lock介面以及Synchronized回顧

JUC之Lock介面以及Synchronized回顧

學習JUC視訊和Java併發程式設計的藝術的總結片段。主要關於Synchronized的回顧以及lock的例項。

Lock介面

Synchronized關鍵字回顧:

多執行緒程式設計步驟(上):

  1. 建立資源類,在資源類建立屬性和操作方法
  2. 建立多個執行緒,呼叫資源類的操作方法

建立執行緒的四種方式:

  1. 繼承Thread
  2. 實現Runnable介面
  3. 使用Callable介面
  4. 使用執行緒池

使用synchronized同步實現售票問題:

只有當資源是空閒的時候,執行緒才能訪問。

/**
 * 建立資源
 */
class ticket{
  private int number = 30;
  public synchronized void sell(){
    if(number >0){
      System.out.println(Thread.currentThread().getName()+":賣出"+number--+"剩餘:"+number);
     }
​
   }
}
​
/**
 * 建立多個執行緒,呼叫資源類的操作方法
 */
public class sellTicket {
  public static void main(String[] args) {
    /**
     * 建立資源
     */
    ticket ticket = new ticket();
    /**
     * 建立三個執行緒(售票員)
     */
    new Thread(new Runnable() {
      public void run() {
        for (int i = 0; i < 40; i++) {
          ticket.sell();
         }
       }
     }, "aaa").start();
    new Thread(new Runnable() {
      public void run() {
        for (int i = 0; i < 40; i++) {
          ticket.sell();
         }
       }
     }, "bbb").start();
    new Thread(new Runnable() {
      public void run() {
        for (int i = 0; i < 40; i++) {
          ticket.sell();
         }
       }
     }, "ccc").start();
   }
}
​

學習《Java併發程式設計的藝術》一節是synchronized的實現原理與應用

Java中的每一個物件都可以作為鎖:

  1. 對於同步方法,鎖的是當前的物件。
  2. 對於靜態方法,鎖的是當前類的class物件。
  3. 對於靜態程式碼塊,Synchonized括號裡配置的物件。

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

那麼鎖到底存在哪裡呢?鎖裡面會儲存什麼資訊呢?

這個問題是該書中的一個問題,對個人來說有點抽象,主要點是:JVM基於進入和退出Monitor(監視器)物件來實現方法同步和程式碼塊同步,但兩者的實現細節不一樣。

程式碼塊同步是使用monitorenter 和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在JVM規範裡並沒有 詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。

monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結 束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有 一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。執行緒執行到monitorenter 指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖。

其中monitorenter主要是獲取監視器鎖,monitorexit主要是釋放監視器鎖。

synchonized中的一個概念是Java物件頭:

synchronized用的鎖是存放在Java物件頭裡面的。

非陣列型別 陣列型別
虛擬機器 3個字寬 2個字寬

需要注意的是:在32位虛擬機器中:1字寬=4位元組,即:32bit

Java物件頭裡的Mark Word裡面預設儲存物件HashCode、分代年齡和鎖標記位。

在執行期間,Mark Word裡面儲存的資料會隨著鎖標誌位的變化而變化。

32位JVM的Mark Word的預設儲存結構與在64位JVM不相同

Lock介面並建立例項:

Java併發程式設計的藝術

在JavaSE5之前使用的是synchronized關鍵字實現鎖功能,5v之後併發包中新增Lock介面以及相關實現類用來實現鎖功能,提供與synchronized類似的同步關係,但是比其更具有靈活性和擴充套件性(擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖)。

檢視Lock的文件得知:

使用 lock 塊來呼叫 try,在之前/之後的構造中,最典型的程式碼如下:

 class X {
  private final ReentrantLock lock = new ReentrantLock();
  // ...
​
  public void m() { 
   lock.lock(); // block until condition holds
   try {
    // ... method body
   } finally {
    lock.unlock()
   }
  }
 }
 

根據上述的程式碼,實現買票程式碼:

import java.util.concurrent.locks.ReentrantLock;
class ticket2{
  private int number = 30;
​
  //建立一個重入鎖
  private ReentrantLock lock = new ReentrantLock();
​
  public void sale(){
    //在內容之前加鎖
    lock.lock();
    if(number >0){
      System.out.println(Thread.currentThread().getName()+":賣出"+number--+"剩餘:"+number);
     }
    //在內容之後解鎖
    lock.unlock();
   }
}
​
public class lockCase {
  public static void main(String[] args) {
    ticket2 tk = new ticket2();
    //建立執行緒
    new Thread(()->{
      for (int i = 0; i < 40; i++) {
        tk.sale();
       }
     },"aaa").start();
    new Thread(()->{
      for (int i = 0; i < 40; i++) {
        tk.sale();
       }
     },"bbb").start();
    new Thread(()->{
      for (int i = 0; i < 40; i++) {
        tk.sale();
       }
     },"bbb").start();
   }
}

上述程式碼中,在ticket2類中存在一個問題就是,當lock中間內容如果出現報錯,那麼後面的程式碼無法執行,也就是鎖無法釋放。所以我們需要將unlock()放到finally中。

目的是保證在獲取到鎖之後,最終能夠被釋放。

public void sale(){
    //在內容之前加鎖
    lock.lock();
    try{
      if(number >0){
        System.out.println(Thread.currentThread().getName()+":賣出"+number--+"剩餘:"+number);
       }
      //在內容之後解鎖
     }finally{
      lock.unlock();
     }
​
   }