Java併發之原子變量及CAS算法-上篇
概述
本文主要講在Java併發編程的時候,如果保證變量的原子性,在JDK提供的類中是怎麼保證變量原子性的呢?。對應Java中的包是:java.util.concurrent.atomic包下。因為涉及到了CAS算法,需要對CAS算法講解及CAS算法三個問題怎麼解決以及和Synchroized比較。文章比較長,所以就分為上下兩個篇幅講解。本文是上篇《Java併發之原子變量及CAS算法-上篇》
本文是《凱哥分享Java併發編程之J.U.C包講解》系列教程中的一篇。如果想系統學習,建議從第一篇開始看。
原子變量案例
在Java中有一種寫法:int i = 10; i++ 這種寫法。
我們先來看看:
輸入的是0還是1呢 ?
I++輸出0的原因分析
答案是:0。為什麼呢?凱哥把編譯後的class文件反編譯,咱們看:
說明:i的操作是i++;y的操作是++y.
從反編譯後的代碼,我們可以看到i++在JVM中的操作,總共分三步:
第一步:聲明變量var10000 ,然後將i賦值給var10000,此時var10000的值是0;
第二步:聲明變量var3 然後把i+1 賦值給var3,此時,var3的值等於1了;
第三步:將變量var10000的值又賦值給了i,此時因為var10000的值是0,所以i的值也是0
所以在sysout(i)的時候,就輸出了0.
我們分析上面1,2,3步驟,可以發現。其實i++執行的是:讀取-修改-重寫 三個操作。
既然讀寫操作,就會涉及到變量原子性。測試在多線程下變量原子性
測試多線程下的變量原子性
那麼,如果我們把對i的操作放到多個線程中操作結果會是什麼樣的呢?
線程操作I的代碼:
開啟十個線程同時操作i的代碼:
我們來看看運行結果:
從運行結果中,我們可以看到,線程Thread-5和線程Thread-8的值是一樣的。
根據上面運行的場景,我們發現,變量i其實是十個線程中的共享變量。從運行的結果來看,多個線程操作後,結果出問題了。
不同線程在內存中運行模擬圖:
線程1;線程2;以及主線程之間運行關係,可以詳見凱哥上一篇文章:《Java併發之內存可見性問題怎麼解決》。這篇文章詳細講解了怎麼關係。
已經看過凱哥上一篇文章或者是知道volatile關鍵字的朋友可能要說,這不就是線程之間變量可見性問題嘛。使用volatile關鍵字修飾i就可以了。真的可以了嗎?
我們修改程序,用volatile來修飾,看看運行結果:
使用volatile關鍵字是否能解決多線程情況下變量原子性呢?
用volatile來修飾變量:
private volatile int shardData = 0;
運行結果:
我們發現,就算使用volatile關鍵字修飾了,依然存在多線程下變量原子性的問題。
怎麼解決這種併發下變量原子性問題呢?
Java的atomic包
在jdk1.5以後,Java為我們提供了一個常用的原子變量。都在:java.util.concureent.atomic包下。我們來看看,都有哪些:
從JDK的API文檔中(凱哥使用的是JDK1.8的API)我們可以看到常用的原子性變量。
怎麼保證原子性呢?
那麼,在atomic包下的這些類怎麼保證原子性呢?
1:該包下的變量都是使用volatile關鍵字來修飾。
解決了多線程之間變量可見性。
Int類型的原子性對象AtomicInteger對象中:
用於對象的AtomicReference對象中:
都是使用volat關鍵字修飾的。
2:使用CAS算法
保持了變量的原子性
總結:
在Java的JDK中提供了concurrent.atomic包,使用這個包下的對象創建的變量就能保證原子性。
保證原子性的策略:
1:變量都是用Volatile關鍵字修飾。來保證內存可見性
2:使用CAS算法,來保證原子性。
下篇預告:
在下一篇文章中,我們主要講解CAS算法原理及CAS算法會參數哪些問題(三個問題)?JDK是怎麼解決的?修改i++使其成為具有原子性變量怎麼實現。
閱讀更多 凱哥java 的文章