Java 執行緒安全 與 鎖

classic123發表於2022-04-23

Java 執行緒安全 與 鎖

多執行緒記憶體模型

  • 執行緒私有棧記憶體
    • 每個執行緒 私有的記憶體區域
  • 程式公有堆記憶體
    • 同一個程式 共有的記憶體區域

為什麼會有執行緒安全問題?

  • 多個執行緒同時具有對同一資源的操作許可權,又發生了同時對該資源進行讀取、寫入的情況,那麼就會出現重複操作的情況

如何解決執行緒安全問題呢? 加鎖

什麼是鎖?

鎖就是對於操作資源的一種許可權

鎖可以做什麼?

對於一個資源加鎖後,每次只能有一個執行緒對該資源進行操作,當該執行緒操作結束後,才會解鎖。
解鎖之後,所有的執行緒獲得競爭此資源的機會。

什麼情況下需要加鎖?

  • 讀讀 不需要加鎖
  • 寫寫 需要加鎖
  • 讀寫 需要加鎖

加鎖的兩種方式(synchronized關鍵字與Lock物件)

第一種:synchronized關鍵字

  • 方法前加synchronized關鍵字

    • 功能:執行緒進入用synchronized宣告的方法時就上鎖,方法執行完自動解鎖,鎖的是當前類的物件
    • 呼叫synchronized宣告的方法一定是排隊執行的
    • 當A執行緒 呼叫object物件的synchronized宣告的X方法時
      • B執行緒可以呼叫其他非synchronized宣告的方法
      • B執行緒不能呼叫其他synchronized宣告的非X方法
  • synchronized鎖重入

    • 鎖重入的概念:自己可以重複獲得自己的內部鎖。即synchronized宣告的方法,可以呼叫本物件的其他synchronized方法。
    • 鎖重入支援繼承的環境,即子類的synchronized方法也可以呼叫父類的synchronized方法。
  • synchronized同步程式碼塊

    • synchronized關鍵字與synchronized程式碼塊的區別

      • synchronized宣告的方法是將當前物件作為鎖
      • synchronized程式碼塊是將任意物件作為鎖
    • 當兩個執行緒訪問同一個物件的synchronized程式碼塊時,只有一個執行緒可以得到執行,另一個執行緒只能等待當前執行緒執行完才能執行。

      • 一半同步,一半非同步
        • 不在synchronized程式碼塊中就是非同步執行,在synchronized程式碼塊中就是同步執行

下面對“一半同步,一半非同步”進行程式碼驗證

  • 建立專案ltl0002 ,檔案Task的程式碼如下:
package ltl0002;

public class Task {

    public void doTask(){
        for (int i = 0; i < 100; i++) {
            System.out.println("no synchronized ThreadName = " + Thread.currentThread().getName() + " i = " + (i+1));
        }
        synchronized (this){
            for (int i = 0; i < 100; i++) {
                System.out.println("synchronized ThreadName = " + Thread.currentThread().getName() + " i = " + (i+1));
            }
        }
        
    }
}
  • 兩個執行緒類程式碼
package ltl0002;

public class MyThread1 implements Runnable{

    private Task task = new Task();

    public MyThread1(Task task){
        this.task = task;
    }

    @Override
    public void run() {

        task.doTask();
    }
}
package ltl0002;

public class MyThread2 implements Runnable{

    private Task task = new Task();

    public MyThread2(Task task){
        this.task = task;
    }

    @Override
    public void run() {

        task.doTask();
    }
}

檔案Run.java程式碼如下:

package ltl0002;

public class Run {
    public static void main(String[] args) {
        Task task = new Task();
        MyThread1 myThread1 = new MyThread1(task);
        MyThread2 myThread2 = new MyThread2(task);
        Thread tr1 = new Thread(myThread1);
        Thread tr2 = new Thread(myThread2);
        tr1.start();
        tr2.start();
    }

}

程式執行結果如圖所示
image

進入synchronized程式碼塊之後,排隊執行,執行結果如圖所示
image

在第一張圖我們可以看到,執行緒0 和 1交叉輸出,說明是非同步進行,而在第二張圖可以看出執行緒0執行完之後,執行緒1才執行,說明它們是同步執行,驗證完畢。

  • 現有三個執行緒,執行緒一對num進行修改,執行緒二三對num進行讀取,如何可以實現,執行緒一與執行緒二三同步執行,而執行緒二三非同步執行呢?
    現在建立專案ltl0003進行測試,Number檔案程式碼如下
package ltl0003;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:53
 * @purpose 成員變數有int num,以及get set方法
 */
public class Number {
  private int num;
  private boolean change = false;

  public int getNum() {
    return num;
  }

  public void setNum(int num) {
    this.num = num;
  }
  public boolean isChangeing(){
    return change;
  }

  public void setChange(boolean change) {
    this.change = change;
  }
}

兩個執行緒類的程式碼如下:

package ltl0003;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:36
 * @purpose 更改num的值
 */
public class MyThread01 implements Runnable{
  static int num = 0;
  Number number;
  public MyThread01(Number num ){
    this.number = num ;
  }
  @Override
  public void run() {
    synchronized (this){
      number.setChange(true);
      for (int i = 0; i < 10000; i++) {
        number.setNum(num++);
      }
      number.setChange(false);
    }
  }
}

package ltl0003;

import static java.lang.Thread.sleep;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:35
 * @purpose 讀取num的值
 */
public class MyThread02 implements Runnable{
  Number number;

  public MyThread02(Number num ){
    this.number = num ;
  }

  @Override
  public void run() {
    for (int i = 0; i < 1000 ; i++) {
      //如果number正在更改,就休眠1ms
      while(number.isChangeing()){
        try {
          sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println(Thread.currentThread().getName()+"的輸出為: num = " + number.getNum());
    }
  }

}

主函式檔案Run程式碼如下:

package ltl0003;
/**
 * @author liTianLu
 * @Date 2022/4/23 15:15
 * @purpose 解決鎖問題 執行緒一對num進行修改,執行緒二三對num進行讀取,此程式碼要實現:執行緒一與執行緒二三同步執行,而執行緒二三非同步執行。
 */
public class Run {
  public static void main(String[] args) {
    Number number = new Number();
    number.setNum(0);
    MyThread01 myThread01 = new MyThread01(number);
    MyThread02 myThread02 = new MyThread02(number);
    Thread tr1 = new Thread(myThread01);
    Thread tr2 = new Thread(myThread02);
    Thread tr3 = new Thread(myThread02);
    tr1.start();
    tr2.start();
    tr3.start();
  }
}

實驗結果如圖所示

image

我們發現,執行緒2/3執行的時候,執行緒1已經執行完畢,且執行緒2、3非同步進行。

第二種:Lock物件的使用

  • ReentrantLock類可以達到與synchronized同樣的效果。
  • 用法:
ReentrantLock lock = new ReentrantLock (); 
lock.lock();//加鎖
lock.unlock();//解鎖
        
//使用try catch finally 可以確保finally 中的程式碼執行,在finally中解鎖
try{
    while(true){
        lock.lock ();
        //操作程式碼
    }
}catch (Exception e) {
    e.printStackTrace();
}finally {
    lock.unlock ();
}

相關文章