Java內存模型之原子性問題


本博客系列是學習併發編程過程當中的記錄總結。因爲文章比較多,寫的時間也比較散,因此我整理了個目錄貼(傳送門),方便查閱。html

併發編程系列博客傳送門java


前言

以前的文章中講到,JMM是內存模型規範在Java語言中的體現。JMM保證了在多核CPU多線程編程環境下,對共享變量讀寫的原子性、可見性和有序性。spring

本文就具體來說講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,可是也有幾回的結果並非咱們預期的。ide

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)this

線程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機制,能保證操做真正的原子性。
相關文章
相關標籤/搜索