Sky's blog

          我和我追逐的夢(mèng)

          常用鏈接

          統(tǒng)計(jì)

          其他鏈接

          友情鏈接

          最新評(píng)論

          編碼最佳實(shí)踐(2)--推薦使用concurrent包中的Atomic類

              這是一個(gè)真實(shí)案例,曾經(jīng)惹出碩大風(fēng)波,故事的起因卻很簡(jiǎn)單,就是需要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的計(jì)數(shù)器,每次取值然后加1,于是就有了下面這段代碼:
                    private int counter = 0;
                    public int getCount ( ) {
                             return counter++;
                    }
              這個(gè)計(jì)數(shù)器被用于生成一個(gè)sessionId,這個(gè)sessionID用于和外部計(jì)費(fèi)系統(tǒng)交互,這個(gè)sessionId理所當(dāng)然的要求保證全局唯一而不重復(fù)。但是很遺憾,上面的代碼最終被發(fā)現(xiàn)會(huì)產(chǎn)生相同的id,因此會(huì)造成一些請(qǐng)求莫名其妙的報(bào)錯(cuò).....更痛苦的是,上面這段代碼是一個(gè)來(lái)自其他部門(mén)開(kāi)發(fā)的工具類,我們當(dāng)時(shí)只是拿了它的jar包來(lái)調(diào)用,沒(méi)有源碼,更沒(méi)有想這里面會(huì)有如此低級(jí)而可怕的錯(cuò)誤。
              由于重復(fù)的sessionId,造成有個(gè)別請(qǐng)求失敗,雖然出現(xiàn)概率極低,經(jīng)常跑一天測(cè)試都不見(jiàn)得能重現(xiàn)一次。因?yàn)槭呛陀?jì)費(fèi)相關(guān),因此哪怕是再低的概率出錯(cuò),也不得不要求解決。實(shí)際情況是,項(xiàng)目開(kāi)發(fā)到最后階段,都開(kāi)始做發(fā)布前最后的穩(wěn)定性測(cè)試了,在7*24小時(shí)的連續(xù)測(cè)試中,這個(gè)問(wèn)題往往在測(cè)試開(kāi)始幾天后才重現(xiàn),將當(dāng)時(shí)負(fù)責(zé)trouble shooting的同事折騰的很慘......經(jīng)過(guò)反復(fù)的查找,終于有人懷疑到這里,反編譯了那個(gè)jar包,才看到上面這段出問(wèn)題的代碼。
              這個(gè)低級(jí)的錯(cuò)誤,源于一個(gè)java的基本知識(shí):
              ++操作,無(wú)論是i++還是++i,都不是原子操作!  
              而一個(gè)非原子操作,在多線程并發(fā)下會(huì)有線程安全的問(wèn)題:這里稍微解釋一下,上面的"++"操作符,從原理上講它其實(shí)包含以下:計(jì)算加1之后的新值,然后將這個(gè)新值賦值給原變量,返回原值。類似于下面的代碼
                    private int counter = 0;
                    public int getCount ( ) {
                             int result = counter;
                             int newValue = counter + 1; // 1. 計(jì)算新值
                             counter = newValue;         // 2. 將新值賦值給原變量
                             return result;
                    }
              多線程并發(fā)時(shí),如果兩個(gè)線程同時(shí)調(diào)用getCount()方法,則他們可能得到相同的counter值。為了保證安全,一個(gè)最簡(jiǎn)單的方法就是在getCount()方法上做同步:
                    private int counter = 0;
                    public synchronized int getCount ( ) {
                             return counter++;
                    }
              這樣就可以避免因++操作符的非原子性而造成的并發(fā)危險(xiǎn)。
              我們?cè)谶@個(gè)案例基礎(chǔ)上稍微再擴(kuò)展一下,如果這里的操作是原子操作,就可以不用同步而安全的并發(fā)訪問(wèn)嗎?我們將這個(gè)代碼稍作修改:
                    private int something = 0;
                    public int getSomething ( ) {
                             return something;
                    }
                    public void setSomething (int something) {
                             this.something = something;
                    }
              假設(shè)有多線程同時(shí)并發(fā)訪問(wèn)getSomething()和setSomething()方法,那么當(dāng)一個(gè)線程通過(guò)調(diào)用setSomething()方法設(shè)置一個(gè)新的值時(shí),其他調(diào)用getSomething()的方法是不是立即可以讀到這個(gè)新值呢?這里的"this.something = something;" 是一個(gè)對(duì)int 類型的賦值,按照java 語(yǔ)言規(guī)范,對(duì)int的賦值是原子操作,這里不存在上面案例中的非原子操作的隱患。
              但是這里還是有一個(gè)重要問(wèn)題,稱為"內(nèi)存可見(jiàn)性"。這里涉及到j(luò)ava內(nèi)存模型的一系列知識(shí),限于篇幅,不詳盡講述,不清楚這些知識(shí)點(diǎn)的可以自己翻翻資料,最簡(jiǎn)單的辦法就是google一下這兩個(gè)關(guān)鍵詞"java 內(nèi)存模型", "java 內(nèi)存可見(jiàn)性"。或者,可以參考這個(gè)帖子"java線程安全總結(jié)", http://www.iteye.com/topic/806990。
              解決這里的"內(nèi)存可見(jiàn)性"問(wèn)題的方式有兩個(gè),一個(gè)是繼續(xù)使用 synchronized 關(guān)鍵字,代碼如下
                    private int something = 0;
                    public synchronized  int getSomething ( ) {
                             return something;
                    }
                    public synchronized  void setSomething (int something) {
                             this.something = something;
                    }
               另一個(gè)是使用volatile 關(guān)鍵字,
                    private volatile int something = 0;
                    public int getSomething ( ) {
                             return something;
                    }
                    public void setSomething (int something) {
                             this.something = something;
                    }
              使用volatile 關(guān)鍵字的方案,在性能上要好很多,因?yàn)関olatile是一個(gè)輕量級(jí)的同步,只能保證多線程的內(nèi)存可見(jiàn)性,不能保證多線程的執(zhí)行有序性。因此開(kāi)銷遠(yuǎn)比synchronized要小。
              讓我們?cè)倩氐介_(kāi)始的案例,因?yàn)槲覀儾捎弥苯釉?getCount() 方法前加synchronized 的修改方式,因此不僅僅避免了非原子性操作帶來(lái)的多線程的執(zhí)行有序性問(wèn)題,也"順帶"解決了內(nèi)存可見(jiàn)性問(wèn)題。
              OK,現(xiàn)在可以繼續(xù)了,前面講到可以通過(guò)在 getCount() 方法前加synchronized 的方式來(lái)解決問(wèn)題,但是其實(shí)還有更方便的方式,可以使用jdk 5.0之后引入的concurrent包中提供的原子類,java.util.concurrent.atomic.Atomic***,如AtomicInteger,AtomicLong等。
                  private AtomicInteger  counter = new AtomicInteger(0);
                  public int getCount ( ) {
                       return counter.incrementAndGet();
                  }
              Atomic類不僅僅提供了對(duì)數(shù)據(jù)操作的線程安全保證,而且提供了一系列的語(yǔ)義清晰的方法如incrementAndGet(),getAndIncrement,addAndGet(),getAndAdd(),使用方便。更重要的是,Atomic類不是一個(gè)簡(jiǎn)單的同步封裝,其內(nèi)部實(shí)現(xiàn)不是簡(jiǎn)單的使用synchronized,而是一個(gè)更為高效的方式CAS (compare and swap) + volatile,從而避免了synchronized的高開(kāi)銷,執(zhí)行效率大為提升。限于篇幅,關(guān)于“CAS”原理就不在這里講訴。
              因此,出于性能考慮,強(qiáng)烈建議盡量使用Atomic類,而不要去寫(xiě)基于synchronized關(guān)鍵字的代碼實(shí)現(xiàn)。
              最后總結(jié)一下,在這個(gè)帖子中我們講訴了一下幾個(gè)問(wèn)題:
              1. ++操作不是原子操作
              2. 非原子操作有線程安全問(wèn)題
              3. 并發(fā)下的內(nèi)存可見(jiàn)性
              4. Atomic類通過(guò)CAS + volatile可以比synchronized做的更高效,推薦使用

          posted on 2012-06-16 17:54 sky ao 閱讀(2910) 評(píng)論(5)  編輯  收藏 所屬分類: java

          評(píng)論

          # re: 編碼最佳實(shí)踐(2)--推薦使用concurrent包中的Atomic類[未登錄](méi) 2012-06-17 08:33 stevenfrog

          感謝!
          清晰,實(shí)用。
          特別是“++操作符”原理那段,我很喜歡。
          一下就說(shuō)清楚了,為什么++會(huì)引起非同步。  回復(fù)  更多評(píng)論   

          # re: 編碼最佳實(shí)踐(2)--推薦使用concurrent包中的Atomic類 2012-06-17 22:18 Yiding He

          生成全局值的操作標(biāo)記為 synchronized 應(yīng)該作為編碼規(guī)范來(lái)執(zhí)行。  回復(fù)  更多評(píng)論   

          # re: 編碼最佳實(shí)踐(2)--推薦使用concurrent包中的Atomic類 2012-06-17 23:18 stevenfrog

          @Yiding He
          我覺(jué)得還是用Atomic類劃算些,synchronized代價(jià)大了些。
          不過(guò)synchronized簡(jiǎn)單,不會(huì)出錯(cuò)。  回復(fù)  更多評(píng)論   

          # re: 編碼最佳實(shí)踐(2)--推薦使用concurrent包中的Atomic類 2012-06-19 09:36 allenny

          嗯,樓主最好出一個(gè)性能測(cè)試比較,這樣比較有說(shuō)服力。  回復(fù)  更多評(píng)論   

          # re: 編碼最佳實(shí)踐(2)--推薦使用concurrent包中的Atomic類[未登錄](méi) 2016-08-22 12:53 飛飛

          原子類操作并不能完全保證多線程下正確啊,執(zhí)行一下 看看結(jié)果就知道了啊  回復(fù)  更多評(píng)論   

          主站蜘蛛池模板: 当雄县| 宜都市| 盐源县| 濮阳市| 安福县| 常州市| 龙里县| 池州市| 南郑县| 安远县| 荥经县| 房山区| 许昌市| 封丘县| 美姑县| 洛川县| 文安县| 潞城市| 织金县| 策勒县| 灵武市| 达州市| 永州市| 佛山市| 界首市| 汉沽区| 德钦县| 西安市| 武鸣县| 龙江县| 金湖县| 昌黎县| 甘谷县| 河源市| 栾城县| 开封县| 依兰县| 铜鼓县| 河北省| 尉犁县| 海南省|