本博客系列是學習併發編程過程當中的記錄總結。因爲文章比較多,寫的時間也比較散,因此我整理了個目錄貼(傳送門),方便查閱。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
上面簡單模擬了一個原子性問題致使程序最終結果出錯的過程。線程
在Java中,對基本數據類型的變量的讀取和賦值操做是原子性操做。
a = true; //原子性 a = 5; //原子性 a = b; //非原子性,分兩步完成,第一步加載b的值,第二步將b賦值給a a = b + 2; //非原子性,分三步完成 a ++; //非原子性,分三步完成
synchronized能夠保證操做結果的原子性。synchronized保證原子性的原理也很簡單,由於synchronized能夠防止多個線程併發執行一段代碼。仍是用上面存款的場景作列子,咱們只須要將存款的方法設置成synchronized的就能保證原子性了。
public synchronized double deposit(double amount){ balance = balance + amount; //1 return balance; }
加了synchronized後,當一個線程沒執行完deposit
這個方法前,其餘線程是不能執行這段代碼的。其實咱們發現synchronized並不能將上面的代碼1編程原子性操做,上面的代碼1仍是有可能被中斷的,可是即便被中斷了其餘線程也不能訪問共享變量balance
,當以前被中斷的線程繼續執行時獲得的結果仍是正確的。
所以synchronized對原子性問題的保證是從最終結果上來保證的,也就是說它只保證最終的結果正確,中間操做的是否被打斷無法保證。這個和CAS操做須要對比着看。
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主要提供了以下的方式來保證操做的原子,保證程序不受原子性問題的影響。