1. 程式人生 > 程式設計 >Java記憶體模型原子性原理及例項解析

Java記憶體模型原子性原理及例項解析

這篇文章主要介紹了Java記憶體模型原子性原理及例項解析,文中通過示例程式碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

本文就具體來講講JMM是如何保證共享變數訪問的原子性的。

原子性問題

原子性是指:一個或多個操作,要麼全部執行且在執行過程中不被任何因素打斷,要麼全部不執行。

下面就是一段會出現原子性問題的程式碼:

public class AtomicProblem {

  private static Logger logger = LoggerFactory.getLogger(AtomicProblem.class);
  public static final int THREAD_COUNT = 10;

  public static void main(String[] args) throws Exception {
    BankAccount sharedAccount = new BankAccount("account-csx",0.00);
    ArrayList<Thread> threads = new ArrayList<>();
    for (int i = 0; i < THREAD_COUNT; i++) {
      Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
          for (int j = 0; j < 1000 ; j++) {
            sharedAccount.deposit(10.00);
          }
        }
      });
      thread.start();
      threads.add(thread);
    }
    for (Thread thread : threads) {
      thread.join();
    }
    logger.info("the balance is:{}",sharedAccount.getBalance());
  }


  public static class BankAccount {
    private String accountName;

    public double getBalance() {
      return balance;
    }

    private double balance;

    public BankAccount(String accountName,double balance){
      this.accountName = accountName;
      this.balance =balance;
    }
    public double deposit(double amount){
      balance = balance + amount;
      return balance;
    }
    public double withdraw(double amount){
      balance = balance - amount;
      return balance;
    }
    public String getAccountName() {
      return accountName;
    }
    public void setAccountName(String accountName) {
      this.accountName = accountName;
    }
  }
}

上面的程式碼中開啟了10個執行緒,每個執行緒會對共享的銀行賬戶進行1000次存款操作,每次存款10塊,所以理論上最後銀行賬戶中的錢應該是10 * 1000 * 10 = 100000塊。我執行了多次上面的程式碼,很多次最後的結果的確是100000,但是也有幾次的結果並不是我們預期的。

14:40:25.981 [main] INFO com.csx.demo.spring.boot.concurrent.jmm.AtomicProblem - the balance is:98260.0

出現上面結果的原因就是因為下面的操作並不是原子操作,其中的balance是一個共享變數。在多執行緒環境下可能會被打斷。

balance = balance + amount;

上面的賦值操作被分為多步執行完成,下面簡單解析下兩個執行緒對balance同時加10的過程(模擬存款過程,假設balance的初始值還是0)

執行緒1從共享記憶體中載入balance的初始值0到工作記憶體
執行緒1對工作記憶體中的值加10

//此時執行緒1的CPU時間耗盡,執行緒2獲得執行機會

執行緒2從共享記憶體中載入balance的初始值到工作記憶體,此時balance的值還是0
執行緒2對工作記憶體中的值加10,此時執行緒2工作記憶體中的副本值是10
執行緒2將balance的副本值重新整理回共享記憶體,此時共享記憶體中balance的值是10

//執行緒2CPU時間片耗盡,執行緒1又獲得執行機會
執行緒1將工作記憶體中的副本值重新整理回共享記憶體,但是此時副本的值還是10,所以最後共享記憶體中的值也是10

上面簡單模擬了一個原子性問題導致程式最終結果出錯的過程。

JMM對原子性問題的保證

自帶原子性保證

在Java中,對基本資料型別的變數的讀取和賦值操作是原子性操作。

a = true; //原子性
a = 5;   //原子性
a = b;   //非原子性,分兩步完成,第一步載入b的值,第二步將b賦值給a
a = b + 2; //非原子性,分三步完成
a ++;   //非原子性,分三步完成

synchronized

synchronized可以保證操作結果的原子性。synchronized保證原子性的原理也很簡單,因為synchronized可以防止多個執行緒併發執行一段程式碼。還是用上面存款的場景做列子,我們只需要將存款的方法設定成synchronized的就能保證原子性了。

 public synchronized double deposit(double amount){
   balance = balance + amount; //1
   return balance;
 }

加了synchronized後,當一個執行緒沒執行完deposit這個方法前,其他執行緒是不能執行這段程式碼的。其實我們發現synchronized並不能將上面的程式碼1程式設計原子性操作,上面的程式碼1還是有可能被中斷的,但是即使被中斷了其他執行緒也不能訪問共享變數balance,當之前被中斷的執行緒繼續執行時得到的結果還是正確的。

因此synchronized對原子性問題的保證是從最終結果上來保證的,也就是說它只保證最終的結果正確,中間操作的是否被打斷沒法保證。這個和CAS操作需要對比著看。

Lock鎖

public double deposit(double amount) {
  readWriteLock.writeLock().lock();
  try {
    balance = balance + amount;
    return balance;
  } finally {
    readWriteLock.writeLock().unlock();
  }
}

Lock鎖保證原子性的原理和synchronized類似,這邊不進行贅述了。

原子操作型別

public static class BankAccount {
  //省略其他程式碼
  private AtomicDouble balance;

  public double deposit(double amount) {
    return balance.addAndGet(amount);
  }
  //省略其他程式碼
} 

JDK提供了很多原子操作類來保證操作的原子性。原子操作類的底層是使用CAS機制的,這個機制對原子性的保證和synchronized有本質的區別。CAS機制保證了整個賦值操作是原子的不能被打斷的,而synchronized值能保證程式碼最後執行結果的正確性,也就是說synchronized能消除原子性問題對程式碼最後執行結果的影響。

簡單總結

在多執行緒程式設計環境下(無論是多核CPU還是單核CPU),對共享變數的訪問存在原子性問題。這個問題可能會導致程式錯誤的執行結果。JMM主要提供瞭如下的方式來保證操作的原子,保證程式不受原子性問題的影響。

  • synchronized機制:保證程式最終正確性,是的程式不受原子性問題的影響;
  • Lock介面:和synchronized類似;
  • 原子操作類:底層使用CAS機制,能保證操作真正的原子性。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。