xylz,imxylz

          關(guān)注后端架構(gòu)、中間件、分布式和并發(fā)編程

             :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            111 隨筆 :: 10 文章 :: 2680 評(píng)論 :: 0 Trackbacks

          在這個(gè)小結(jié)里面重點(diǎn)討論原子操作的原理和設(shè)計(jì)思想。

          由于在下一個(gè)章節(jié)中會(huì)談到鎖機(jī)制,因此此小節(jié)中會(huì)適當(dāng)引入鎖的概念。

          Java Concurrency in Practice中是這樣定義線程安全的:

          當(dāng)多個(gè)線程訪問(wèn)一個(gè)類時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替運(yùn)行,并且不需要額外的同步及在調(diào)用方代碼不必做其他的協(xié)調(diào),這個(gè)類的行為仍然是正確的,那么這個(gè)類就是線程安全的。

          顯然只有資源競(jìng)爭(zhēng)時(shí)才會(huì)導(dǎo)致線程不安全,因此無(wú)狀態(tài)對(duì)象永遠(yuǎn)是線程安全的

          原子操作的描述是: 多個(gè)線程執(zhí)行一個(gè)操作時(shí),其中任何一個(gè)線程要么完全執(zhí)行完此操作,要么沒(méi)有執(zhí)行此操作的任何步驟,那么這個(gè)操作就是原子的。

          枯燥的定義介紹完了,下面說(shuō)更枯燥的理論知識(shí)。

          指令重排序

          Java語(yǔ)言規(guī)范規(guī)定了JVM線程內(nèi)部維持順序化語(yǔ)義,也就是說(shuō)只要程序的最終結(jié)果等同于它在嚴(yán)格的順序化環(huán)境下的結(jié)果,那么指令的執(zhí)行順序就可能與代碼的順序不一致。這個(gè)過(guò)程通過(guò)叫做指令的重排序。指令重排序存在的意義在于:JVM能夠根據(jù)處理器的特性(CPU的多級(jí)緩存系統(tǒng)、多核處理器等)適當(dāng)?shù)闹匦屡判驒C(jī)器指令,使機(jī)器指令更符合CPU的執(zhí)行特點(diǎn),最大限度的發(fā)揮機(jī)器的性能。

          程序執(zhí)行最簡(jiǎn)單的模型是按照指令出現(xiàn)的順序執(zhí)行,這樣就與執(zhí)行指令的CPU無(wú)關(guān),最大限度的保證了指令的可移植性。這個(gè)模型的專業(yè)術(shù)語(yǔ)叫做順序化一致性模型。但是現(xiàn)代計(jì)算機(jī)體系和處理器架構(gòu)都不保證這一點(diǎn)(因?yàn)槿藶榈闹付ú⒉荒芸偸潜WC符合CPU處理的特性)。

          我們來(lái)看最經(jīng)典的一個(gè)案例。

          package xylz.study.concurrency.atomic;

          public class ReorderingDemo {

             
          static int x = 0, y = 0, a = 0, b = 0;

             
          public static void main(String[] args) throws Exception {

                 
          for (int i = 0; i < 100; i++) {
                      x
          =y=a=b=0;
                      Thread one
          = new Thread() {
                         
          public void run() {
                              a
          = 1;
                              x
          = b;
                          }

                      }
          ;
                      Thread two
          = new Thread() {
                         
          public void run() {
                              b
          = 1;
                              y
          = a;
                          }

                      }
          ;
                      one.start();
                      two.start();
                      one.join();
                      two.join();
                      System.out.println(x
          + " " + y);
                  }

              }
           

          }



          在這個(gè)例子中one/two兩個(gè)線程修改區(qū)x,y,a,b四個(gè)變量,在執(zhí)行100次的情況下,可能得到(0 1)或者(1 0)或者(1 1)。事實(shí)上按照J(rèn)VM的規(guī)范以及CPU的特性有很可能得到(0 0)。當(dāng)然上面的代碼大家不一定能得到(0 0),因?yàn)閞un()里面的操作過(guò)于簡(jiǎn)單,可能比啟動(dòng)一個(gè)線程花費(fèi)的時(shí)間還少,因此上面的例子難以出現(xiàn)(0,0)。但是在現(xiàn)代CPU和JVM上確實(shí)是存在的。由于run()里面的動(dòng)作對(duì)于結(jié)果是無(wú)關(guān)的,因此里面的指令可能發(fā)生指令重排序,即使是按照程序的順序執(zhí)行,數(shù)據(jù)變化刷新到主存也是需要時(shí)間的。假定是按照a=1;x=b;b=1;y=a;執(zhí)行的,x=0是比較正常的,雖然a=1在y=a之前執(zhí)行的,但是由于線程one執(zhí)行a=1完成后還沒(méi)有來(lái)得及將數(shù)據(jù)1寫(xiě)回主存(這時(shí)候數(shù)據(jù)是在線程one的堆棧里面的),線程two從主存中拿到的數(shù)據(jù)a可能仍然是0(顯然是一個(gè)過(guò)期數(shù)據(jù),但是是有可能的),這樣就發(fā)生了數(shù)據(jù)錯(cuò)誤。

          在兩個(gè)線程交替執(zhí)行的情況下數(shù)據(jù)的結(jié)果就不確定了,在機(jī)器壓力大,多核CPU并發(fā)執(zhí)行的情況下,數(shù)據(jù)的結(jié)果就更加不確定了。

          Happens-before法則

          Java存儲(chǔ)模型有一個(gè)happens-before原則,就是如果動(dòng)作B要看到動(dòng)作A的執(zhí)行結(jié)果(無(wú)論A/B是否在同一個(gè)線程里面執(zhí)行),那么A/B就需要滿足happens-before關(guān)系。

          在介紹happens-before法則之前介紹一個(gè)概念:JMM動(dòng)作(Java Memeory Model Action),Java存儲(chǔ)模型動(dòng)作。一個(gè)動(dòng)作(Action)包括:變量的讀寫(xiě)、監(jiān)視器加鎖和釋放鎖、線程的start()和join()。后面還會(huì)提到鎖的的。

          happens-before完整規(guī)則:

          (1)同一個(gè)線程中的每個(gè)Action都happens-before于出現(xiàn)在其后的任何一個(gè)Action。

          (2)對(duì)一個(gè)監(jiān)視器的解鎖happens-before于每一個(gè)后續(xù)對(duì)同一個(gè)監(jiān)視器的加鎖。

          (3)對(duì)volatile字段的寫(xiě)入操作happens-before于每一個(gè)后續(xù)的同一個(gè)字段的讀操作。

          (4)Thread.start()的調(diào)用會(huì)happens-before于啟動(dòng)線程里面的動(dòng)作。

          (5)Thread中的所有動(dòng)作都happens-before于其他線程檢查到此線程結(jié)束或者Thread.join()中返回或者Thread.isAlive()==false。

          (6)一個(gè)線程A調(diào)用另一個(gè)另一個(gè)線程B的interrupt()都happens-before于線程A發(fā)現(xiàn)B被A中斷(B拋出異常或者A檢測(cè)到B的isInterrupted()或者interrupted())。

          (7)一個(gè)對(duì)象構(gòu)造函數(shù)的結(jié)束happens-before與該對(duì)象的finalizer的開(kāi)始

          (8)如果A動(dòng)作happens-before于B動(dòng)作,而B(niǎo)動(dòng)作happens-before與C動(dòng)作,那么A動(dòng)作happens-before于C動(dòng)作。

          volatile語(yǔ)義

          到目前為止,我們多次提到volatile,但是卻仍然沒(méi)有理解volatile的語(yǔ)義。

          volatile相當(dāng)于synchronized的弱實(shí)現(xiàn),也就是說(shuō)volatile實(shí)現(xiàn)了類似synchronized的語(yǔ)義,卻又沒(méi)有鎖機(jī)制。它確保對(duì)volatile字段的更新以可預(yù)見(jiàn)的方式告知其他的線程。

          volatile包含以下語(yǔ)義:

          (1)Java 存儲(chǔ)模型不會(huì)對(duì)valatile指令的操作進(jìn)行重排序:這個(gè)保證對(duì)volatile變量的操作時(shí)按照指令的出現(xiàn)順序執(zhí)行的。

          (2)volatile變量不會(huì)被緩存在寄存器中(只有擁有線程可見(jiàn))或者其他對(duì)CPU不可見(jiàn)的地方,每次總是從主存中讀取volatile變量的結(jié)果。也就是說(shuō)對(duì)于volatile變量的修改,其它線程總是可見(jiàn)的,并且不是使用自己線程棧內(nèi)部的變量。也就是在happens-before法則中,對(duì)一個(gè)valatile變量的寫(xiě)操作后,其后的任何讀操作理解可見(jiàn)此寫(xiě)操作的結(jié)果。

          盡管volatile變量的特性不錯(cuò),但是volatile并不能保證線程安全的,也就是說(shuō)volatile字段的操作不是原子性的,volatile變量只能保證可見(jiàn)性(一個(gè)線程修改后其它線程能夠理解看到此變化后的結(jié)果),要想保證原子性,目前為止只能加鎖!

          volatile通常在下面的場(chǎng)景:

           

          volatile boolean done = false;



             
          while( ! done ){
                  dosomething();
              }

           

          應(yīng)用volatile變量的三個(gè)原則:

          (1)寫(xiě)入變量不依賴此變量的值,或者只有一個(gè)線程修改此變量

          (2)變量的狀態(tài)不需要與其它變量共同參與不變約束

          (3)訪問(wèn)變量不需要加鎖

           

          這一節(jié)理論知識(shí)比較多,但是這是很面很多章節(jié)的基礎(chǔ),在后面的章節(jié)中會(huì)多次提到這些特性。

          本小節(jié)中還是沒(méi)有談到原子操作的原理和思想,在下一節(jié)中將根據(jù)上面的一些知識(shí)來(lái)介紹原子操作。

           

          參考資料:

          (1)Java Concurrency in Practice

          (2)正確使用 Volatile 變量

           



          ©2009-2014 IMXYLZ |求賢若渴
          posted on 2010-07-03 20:40 imxylz 閱讀(46628) 評(píng)論(16)  編輯  收藏 所屬分類: J2EEJava Concurrency

          評(píng)論

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 2010-07-04 00:41 滴水
          講到這篇開(kāi)始有點(diǎn)意思了,呵呵,加油。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-07-23 16:37 thebye85
          “volatile并不能保證線程安全的,也就是說(shuō)volatile字段的操作不是原子性的”
          既然volatile保證變量在主內(nèi)存,多線程操作的應(yīng)該在同一內(nèi)存,能說(shuō)下為什么多線程下不是原子性的嗎?謝謝  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-07-23 16:56 xylz
          @thebye85
          比如主存中是i=10,兩個(gè)線程同時(shí)讀取到i=10,都需要++,那么調(diào)用完成后可能i=11,而我們的目標(biāo)是i=12,所以不是一致的。如果一個(gè)加一個(gè)減結(jié)果就更加難以預(yù)料了。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-07-23 22:36 thebye85
          @xylz
          謝謝回復(fù)。再請(qǐng)教下兩個(gè)線程中的i++后的中間臨時(shí)結(jié)果(在沒(méi)寫(xiě)入到i之前),是保存在哪的,是在主存中而新開(kāi)辟的內(nèi)存嗎?  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-07-23 23:07 xylz
          @thebye85

          Java內(nèi)存分為堆和棧。而棧通常是線程獨(dú)有的,堆是線程共享的,通常對(duì)于一個(gè)變量而言,JVM盡可能的保存在棧中,因?yàn)闂MǔJ羌拇嫫鳌PU緩存的高速設(shè)備,所以棧一般比較小,而堆比較大一般在內(nèi)存中。對(duì)于一個(gè)volatile變量,JMM(Java存儲(chǔ)模型)保證對(duì)所有線程是共享,所以一定不會(huì)存在寄存器、CPU緩存等棧上,我查了英文本的《The Java Language Specification, Third Editon》,上面沒(méi)有說(shuō)具體存放于何處,我猜就可能存放于主存上,也就是堆上。

          所以通常而言,操作一個(gè)volatile變量要比非volatile變量開(kāi)銷要大,但是這個(gè)差別很小,相對(duì)于鎖機(jī)制來(lái)說(shuō)可以忽略不計(jì)。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-07-31 12:53 Johnny Jian
          @thebye85
          應(yīng)該是操作棧吧  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-08-12 15:20 thebye85
          @Johnny Jian
          我是說(shuō)假如變量i聲明為volatile,多線程操作i都是在共享內(nèi)存(堆)上吧?
          保存各自線程棧的話,怎么保證i的可見(jiàn)性?  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2010-08-12 15:27 Johnny Jian
          @thebye85
          i++是非原子操作,你只能通過(guò)其他手段來(lái)保證咯  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2011-07-09 23:24 ieye
          @xylz
          我覺(jué)得操作的中間結(jié)果應(yīng)該還是在線程棧中的,這個(gè)計(jì)算結(jié)果又沒(méi)有必要對(duì)其他線程可見(jiàn),只有寫(xiě)回主存的時(shí)候才對(duì)其他線程可見(jiàn)。個(gè)人理解  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2011-08-02 08:36 喜樂(lè)
          @ieye
          并且寫(xiě)回主存的時(shí)候不是原子的, 這就是問(wèn)題所在。 比如一個(gè)LONG, 在32位機(jī)上可以寫(xiě)高位也可以先寫(xiě)低位。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2011-08-09 14:07 那時(shí)花開(kāi)
          在多核的環(huán)境中,高速緩存有多個(gè),如果這個(gè)變量不在主存存放的話,那么線程讀到的值就可能是錯(cuò)的,未及時(shí)同步的。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2011-11-24 17:03 duanjb
          我認(rèn)為局部變量才是保持在棧里面的,對(duì)于全局變量則應(yīng)該是放在堆里面了!要不然沒(méi)法保證可見(jiàn)性。
          volatile boolean done = false;



          while( ! done ){
          dosomething();
          }

          對(duì)于這個(gè)問(wèn)題,<<effective java>中說(shuō)明如果不加volatile,程序會(huì)被jvm優(yōu)化為
          if(!done)
          {
          while(true)
          {
          dosomething();
          }
          }
          這意味一旦進(jìn)入到while中就會(huì)死循環(huán)。。。。。。
          但是在多核cpu下,我的測(cè)試是就算進(jìn)入了while循環(huán)也會(huì)停止掉!
          http://www.iteye.com/topic/1118193
          這個(gè)帖子有不少人討論  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2012-01-19 10:35 jasonlmq
          申明為volatile的屬性變量,是存儲(chǔ)在堆的主存中。無(wú)論任何線程訪問(wèn)此變量都是直接從主內(nèi)存中讀取,不是從線程自己的堆中取,更不是從線程的棧中讀取。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2012-10-25 12:34 vitamin.x
          @thebye85
          雖然增量操作(x++)看上去類似一個(gè)單獨(dú)操作,實(shí)際上它是一個(gè)由讀取-修改-寫(xiě)入操作序列組成的組合操作,必須以原子方式執(zhí)行,而 volatile 不能提供必須的原子特性。實(shí)現(xiàn)正確的操作需要使 x 的值在操作期間保持不變,而 volatile 變量無(wú)法實(shí)現(xiàn)這點(diǎn)。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則 2013-01-08 18:26 mgampkay
          對(duì)volatile變量的讀會(huì)從內(nèi)存讀入寄存器,寫(xiě)會(huì)把寄存器內(nèi)容寫(xiě)回內(nèi)存。
          可見(jiàn)性就是指我修改后你再讀,你讀到的就是我修改后的內(nèi)容。

          例如有一個(gè)volatile變量a,初值為0,依次執(zhí)行a = 1; a = 2;這樣就有兩次對(duì)內(nèi)存的寫(xiě)入。在a = 1后,如果有線程讀a,就會(huì)從內(nèi)存里都到1。

          對(duì)非volatile變量b, 依次執(zhí)行b = 1; b = 2; 第一次修改可能只是對(duì)寄存器修改,而沒(méi)有寫(xiě)回內(nèi)存。這樣執(zhí)行b = 1后,另一個(gè)線程讀b就會(huì)都到內(nèi)存中的0。

          所以說(shuō)volatile變量可以保證可見(jiàn)性。  回復(fù)  更多評(píng)論
            

          # re: 深入淺出 Java Concurrency (4): 原子操作 part 3 指令重排序與happens-before法則[未登錄](méi) 2013-03-03 14:34 teasp
          博主關(guān)于hanppens-before規(guī)則的第5條有錯(cuò)誤。  回復(fù)  更多評(píng)論
            


          ©2009-2014 IMXYLZ
          主站蜘蛛池模板: 双柏县| 宝清县| 方城县| 沅江市| 长沙市| 扎囊县| 博客| 寿光市| 繁峙县| 长沙县| 中牟县| 利川市| 汝南县| 蕲春县| 浦县| 沭阳县| 额尔古纳市| 通江县| 鄱阳县| 乌兰浩特市| 武清区| 郁南县| 海门市| 皋兰县| 吴江市| 钦州市| 永年县| 陆丰市| 庆安县| 连平县| 谢通门县| 芦溪县| 资中县| 汉中市| 历史| 孝义市| 望江县| 高密市| 龙口市| 汕头市| 丹寨县|