java之concurrent包中的Atomic類

java之concurrent包中的Atomic類

sky ao IT哈哈
這是一個真實案例,曾經惹出碩大風波,故事的原由卻很簡單,就是須要實現一個簡單的計數器,每次取值而後加1,因而就有了下面這段代碼:java

private int counter = 0;
          public int getCount ( ) {
                   return counter++;
          }

這個計數器被用於生成一個sessionId,這個sessionID用於和外部計費系統交互,這個sessionId理所固然的要求保證全局惟一而不重複。可是很遺憾,上面的代碼最終被發現會產生相同的id,所以會形成一些請求莫名其妙的報錯.....更痛苦的是,上面這段代碼是一個來自其餘部門開發的工具類,咱們當時只是拿了它的jar包來調用,沒有源碼,更沒有想這裏面會有如此低級而可怕的錯誤。安全

因爲重複的sessionId,形成有個別請求失敗,雖然出現機率極低,常常跑一天測試都不見得能重現一次。由於是和計費相關,所以哪怕是再低的機率出錯,也不得不要求解決。實際狀況是,項目開發到最後階段,都開始作發佈前最後的穩定性測試了,在7*24小時的連續測試中,這個問題每每在測試開始幾天後才重現,將當時負責trouble shooting的同事折騰的很慘......通過反覆的查找,終於有人懷疑到這裏,反編譯了那個jar包,纔看到上面這段出問題的代碼。session

這個低級的錯誤,源於一個java的基本知識:
++操做,不管是i++仍是++i,都不是原子操做! 多線程

而一個非原子操做,在多線程併發下會有線程安全的問題:這裏稍微解釋一下,上面的"++"操做符,從原理上講它其實包含如下:計算加1以後的新值,而後將這個新值賦值給原變量,返回原值。相似於下面的代碼併發

private int counter = 0;
          public int getCount ( ) {
                   int result = counter;
                   int newValue = counter + 1; // 1. 計算新值
                   counter = newValue;         // 2. 將新值賦值給原變量
                   return result;
          }

多線程併發時,若是兩個線程同時調用getCount()方法,則他們可能獲得相同的counter值。爲了保證安全,一個最簡單的方法就是在getCount()方法上作同步:ide

private int counter = 0;
          public synchronized int getCount ( ) {
                   return counter++;
          }

這樣就能夠避免因++操做符的非原子性而形成的併發危險。
咱們在這個案例基礎上稍微再擴展一下,若是這裏的操做是原子操做,就能夠不用同步而安全的併發訪問嗎?咱們將這個代碼稍做修改:工具

private int something = 0;
          public int getSomething ( ) {
                   return something;
          }
          public void setSomething (int something) {
                   this.something = something;
          }

假設有多線程同時併發訪問getSomething()和setSomething()方法,那麼當一個線程經過調用setSomething()方法設置一個新的值時,其餘調用getSomething()的方法是否是當即能夠讀到這個新值呢?這裏的"this.something = something;" 是一個對int 類型的賦值,按照java 語言規範,對int的賦值是原子操做,這裏不存在上面案例中的非原子操做的隱患。性能

可是這裏仍是有一個重要問題,稱爲"內存可見性"。這裏涉及到java內存模型的一系列知識,限於篇幅,不詳盡講述,不清楚這些知識點的能夠本身翻翻資料,最簡單的辦法就是google一下這兩個關鍵詞"java 內存模型", "java 內存可見性"。或者,能夠參考這個帖子"java線程安全總結",測試

解決這裏的"內存可見性"問題的方式有兩個,一個是繼續使用 synchronized 關鍵字,代碼以下this

private int something = 0;
          public synchronized  int getSomething ( ) {
                   return something;
          }
          public synchronized  void setSomething (int something) {
                   this.something = something;
          }
另外一個是使用volatile 關鍵字,
private volatile int something = 0;
          public int getSomething ( ) {
                   return something;
          }
          public void setSomething (int something) {
                   this.something = something;
          }

使用volatile 關鍵字的方案,在性能上要好不少,由於volatile是一個輕量級的同步,只能保證多線程的內存可見性,不能保證多線程的執行有序性。所以開銷遠比synchronized要小。

讓咱們再回到開始的案例,由於咱們採用直接在 getCount() 方法前加synchronized 的修改方式,所以不只僅避免了非原子性操做帶來的多線程的執行有序性問題,也"順帶"解決了內存可見性問題。

OK,如今能夠繼續了,前面講到能夠經過在 getCount() 方法前加synchronized 的方式來解決問題,可是其實還有更方便的方式,可使用jdk 5.0以後引入的concurrent包中提供的原子類,java.util.concurrent.atomic.Atomic***,如AtomicInteger,AtomicLong等。

private AtomicInteger  counter = new AtomicInteger(0);
        public int getCount ( ) {
             return counter.incrementAndGet();
        }

Atomic類不只僅提供了對數據操做的線程安全保證,並且提供了一系列的語義清晰的方法如incrementAndGet(),getAndIncrement,addAndGet(),getAndAdd(),使用方便。更重要的是,Atomic類不是一個簡單的同步封裝,其內部實現不是簡單的使用synchronized,而是一個更爲高效的方式CAS (compare and swap) + volatile,從而避免了synchronized的高開銷,執行效率大爲提高。限於篇幅,關於「CAS」原理就不在這裏講訴。

所以,出於性能考慮,強烈建議儘可能使用Atomic類,而不要去寫基於synchronized關鍵字的代碼實現。
最後總結一下,在這個帖子中咱們講述了一下幾個問題:
1. ++操做不是原子操做
2. 非原子操做有線程安全問題
3. 併發下的內存可見性
4. Atomic類經過CAS + volatile能夠比synchronized作的更高效,推薦使用

相關文章
相關標籤/搜索