Picses' sky

          Picses' sky
          posts - 43, comments - 29, trackbacks - 0, articles - 24
          摘要
          開發(fā)者有時(shí)創(chuàng)建的多線程程序會(huì)生成錯(cuò)誤值或產(chǎn)生其它奇怪的行為。古怪行為一般出現(xiàn)在一個(gè)多線程程序沒使用同步連載線程訪問關(guān)鍵代碼部份的時(shí)候。同步連載線程訪問關(guān)鍵代碼部份是什么意思呢?在這篇文章中解釋了同步,Java的同步機(jī)制,以及當(dāng)開發(fā)者沒有正確使用這個(gè)機(jī)制時(shí)出現(xiàn)的兩個(gè)問題。一旦你看完這篇文章,你就可以避免在你的多線程Java程序中因缺乏同步而產(chǎn)生的奇怪行為。
          創(chuàng)建多線程Java程序難嗎??jī)H從《用Java線程獲取優(yōu)異性能(I)》中獲得的信息你就可以回答,不。畢竟,我已經(jīng)向你顯示了如何輕松地創(chuàng)建線程對(duì)象,通過調(diào)用Thread的start()方法起動(dòng)與這些對(duì)象相關(guān)的線程,以及通過調(diào)用其它Thread方法,比如三個(gè)重載的join()方法執(zhí)行簡(jiǎn)單的線程操作。至今仍有許多開發(fā)者在開發(fā)一些多線程程序時(shí)面臨困難境遇。他們的程序經(jīng)常功能不穩(wěn)定或產(chǎn)生錯(cuò)誤值。例如,一個(gè)多線程程序可能將不正確的雇員資料存貯在數(shù)據(jù)庫中,比如姓名和地址。姓名可能屬于一個(gè)雇員的,而地址卻屬于另一個(gè)的。是什么引起這種奇怪行為的呢? 是缺乏同步:連載行為,或在同一時(shí)間排序,線程訪問那些讓多重線程操作的類和字段變量實(shí)例的代碼序列,以及其他共享資源。我稱這些代碼序列為關(guān)鍵代碼部份。
          注意:不象類和實(shí)例字段變量,線程不能共享本地變量和參數(shù)。原因是:本地變量和參數(shù)在一個(gè)線程方法中分配——叫堆棧。結(jié)果,每一個(gè)線程都收到它自己對(duì)那些變量的拷貝。相反,線程能夠共享類字段和實(shí)例字段因?yàn)槟切┳兞吭谝粋€(gè)線程方法(叫堆棧)中沒有被分配。取而代之,它們作為類(類字段)或?qū)ο螅▽?shí)例字段)的一部份在共享內(nèi)存堆中被分配。
          這篇文章將教你如何使用同步連載線程訪問關(guān)鍵代碼部份。我用一個(gè)說明為什么一些多線程程序必須使用同步的例子作為開始。我接下來就監(jiān)視器和鎖探討Java的同步機(jī)制和synchronized 關(guān)鍵字。我通過研究由這樣的錯(cuò)用產(chǎn)生的兩個(gè)問題判定常常因?yàn)椴徽_的使用同步機(jī)制而否認(rèn)了它的好處。
          閱讀關(guān)于線程程序的整個(gè)系列:
          · 第I部份:介紹線程、線程類及Runnable
          · 第II部份:使用同步連載線程訪問關(guān)鍵代碼部份
          對(duì)于同步的需要
          為什么我們需要同步呢?一種回答,考慮這個(gè)例子:你寫一個(gè)使用一對(duì)線程模擬取款/存款金融事務(wù)的Java程序。在那個(gè)程序中,一個(gè)線程處理存款,同時(shí)其它線程正處理取款。每一個(gè)線程操作一對(duì)共享變量、類及實(shí)例字段變量,這些用來標(biāo)識(shí)金融事務(wù)的姓名和賬號(hào)。對(duì)于一個(gè)正確的金融事務(wù),每一個(gè)線程必須在其它線程開始給name和amount賦值前(并且同時(shí)打印那些值)給name和amount變量賦值(并打印那些值,模擬存貯事務(wù))。其源代碼如下:
          列表1. NeedForSynchronizationDemo.java
          // NeedForSynchronizationDemo.java
          class NeedForSynchronizationDemo
          {
          public static void main (String [] args)
          {
          FinTrans ft = new FinTrans ();
          TransThread tt1 = new TransThread (ft, "Deposit Thread");
          TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
          tt1.start ();
          tt2.start ();
          }
          }
          class FinTrans
          {
          public static String transName;
          public static double amount;
          }
          class TransThread extends Thread
          {
          private FinTrans ft;
          TransThread (FinTrans ft, String name)
          {
          super (name); //保存線程名稱
          this.ft = ft; //保存對(duì)金融事務(wù)對(duì)象的引用
          }
          public void run ()
          {
          for (int i = 0; i < 100; i++)
          {
          if (getName ().equals ("Deposit Thread"))
          {
          //存款線程關(guān)鍵代碼部份的開始
          ft.transName = "Deposit";
          try
          {
          Thread.sleep ((int) (Math.random () * 1000));
          }
          catch (InterruptedException e)
          {
          }
          ft.amount = 2000.0;
          System.out.println (ft.transName + " " + ft.amount);
          //存款線程關(guān)鍵代碼部份的結(jié)束
          }
          else
          {
          //取款線程關(guān)鍵代碼部份的開始
          ft.transName = "Withdrawal";
          try
          {
          Thread.sleep ((int) (Math.random () * 1000));
          }
          catch (InterruptedException e)
          {
          }
          ft.amount = 250.0;
          System.out.println (ft.transName + " " + ft.amount);
          //取款線程關(guān)鍵代碼部份的結(jié)束
          }
          }
          }
          }
          NeedForSynchronizationDemo的源代碼有兩個(gè)關(guān)鍵代碼部份:一個(gè)可理解為存款線程,另一個(gè)可理解為取款線程。在存款線程關(guān)鍵代碼部份中,線程分配Deposit String對(duì)象的引用給共享變量transName及分配2000.0 給共享變量amount。同樣,在取款關(guān)鍵代碼部份,線程分配Withdrawal String對(duì)象的引用給transName及分配250.0給amount。在每個(gè)線程的分配之后打印那些變量的內(nèi)容。當(dāng)你運(yùn)行NeedForSynchronizationDemo時(shí),你可能期望輸出類似于Withdrawal 250.0 和Deposit 2000.0兩行組成的列表。相反,你收到的輸出如下所示:
          Withdrawal 250.0
          Withdrawal 2000.0
          Deposit 2000.0
          Deposit 2000.0
          Deposit 250.0
          程序明顯有問題。取款線程不應(yīng)該模擬$2,000的取款,存款線程不應(yīng)該模擬$250的存款。每一個(gè)線程產(chǎn)生不一致的輸出。是什么引起了這些矛盾呢?我們是如下認(rèn)為的:
          · 在一個(gè)單處理器機(jī)器上,線程共享處理器。結(jié)果,一個(gè)線程僅能執(zhí)行一定時(shí)間段。在其它時(shí)間里, JVM/操作系統(tǒng)暫停那個(gè)線程的執(zhí)行并允許其它線程執(zhí)行——一種線程時(shí)序安排。在一個(gè)多處理器機(jī)器上,依靠線程和處理器的數(shù)目,每一個(gè)線程都能擁有它自己的處理器。
          · 在一單處理器機(jī)器上,一個(gè)線程的執(zhí)行時(shí)間段沒有足夠長(zhǎng)到在其它線程開始執(zhí)行的關(guān)鍵代碼部份前完成它自己的關(guān)鍵代碼部分。在一個(gè)多處理器機(jī)器上,線程能夠同時(shí)執(zhí)行它們自己的關(guān)鍵代碼部份。然而,它們可能在不同的時(shí)間進(jìn)入它們的關(guān)鍵代碼部份。
          · 無論是單處理器或是多處理器機(jī)器,下面的情形都可能發(fā)生:線程A在它的關(guān)鍵代碼部份分配一個(gè)值給共享變量X并決定執(zhí)行一個(gè)要求100毫秒的輸入/輸出操作。接下來線程B進(jìn)入它的關(guān)鍵代碼部份,分配一個(gè)不同的值給X,執(zhí)行一個(gè)50毫秒的輸入/輸出操作并分配值給共享變量Y 和Z。線程A的輸入/輸出操作完成,并分配它自己的值給Y和Z。因?yàn)閄包含一個(gè)B分配的值,然而Y和Z包含A分配的值,這是一個(gè)矛盾的結(jié)果。
          這個(gè)矛盾是怎樣在NeedForSynchronizationDemo中產(chǎn)生的呢?假設(shè)存款線程執(zhí)行ft.transName = "Deposit"并且接下來調(diào)用Thread.sleep()。在那一點(diǎn),存款線程交出處理器控制一段時(shí)間進(jìn)行休眠,讓取款線程執(zhí)行。假定存款線程休眠500毫秒(感謝Math.random()從0到999毫秒范圍隨機(jī)選取一個(gè)值)。在存款線程休眠期間,取款線程執(zhí)行ft.transName = "Withdrawal",休眠50毫秒 (取款線程隨機(jī)選取休眠值),醒后執(zhí)行ft.amount = 250.0并執(zhí)行System.out.println (ft.transName + " " + ft.amount)—所有都在存款線程醒來之前。結(jié)果,取款線程打印Withdrawal 250.0,那是正確的。當(dāng)存款線程醒來執(zhí)行ft.amount = 2000.0,接下來執(zhí)行System.out.println (ft.transName + " " + ft.amount)。這個(gè)時(shí)間Withdrawal 2000.0 打印,那是不正確的。雖然存款線程先前分配"Deposit"的引用給transName,但這個(gè)引用隨后會(huì)在取款線程分配”Withdrawal”引用給那個(gè)共享變量時(shí)消失。當(dāng)存款線程醒來時(shí),它就不能存貯正確的引用到transName,但通過分配2000.0給amount繼續(xù)它的執(zhí)行。雖然兩個(gè)變量都不會(huì)有無效的值,但它們的結(jié)合值卻是矛盾的。假如這樣的話,它們的值顯示企圖取款$2,000。
          很久以前,計(jì)算機(jī)科學(xué)家發(fā)明了描述導(dǎo)致矛盾的多線程組合行為的一個(gè)術(shù)語。術(shù)語是競(jìng)態(tài)條件(race condition)—每一個(gè)線程競(jìng)相在其它線程進(jìn)入同一關(guān)鍵代碼部份前完成它自己的關(guān)鍵代碼部份的行為。作為NeedForSynchronizationDemo示范,線程的執(zhí)行順序是不可知的。這里不能保證一個(gè)線程能夠在其它線程進(jìn)入關(guān)鍵代碼部份前完成它自己的關(guān)鍵代碼部份。因此,我們會(huì)有競(jìng)態(tài)條件引起不一致。要阻止競(jìng)態(tài)條件,每一個(gè)線程必須在其它線程進(jìn)入同一關(guān)鍵代碼部份或其它操作同一共享變量或資源的相關(guān)關(guān)鍵代碼部份前完成它自己的關(guān)鍵代碼部份。對(duì)于一個(gè)關(guān)鍵代碼部份沒有連載訪問方法(即是在一個(gè)時(shí)間只允許訪問一個(gè)線程),你就不能阻止競(jìng)態(tài)條件或不一致的出現(xiàn)。幸運(yùn)的是,Java提供了連載線程訪問的方法:通過它的同步機(jī)制。
          注意:對(duì)于Java的類型,只有長(zhǎng)整型和雙精度浮點(diǎn)型變量?jī)A向于不一致。為什么?一個(gè)32位JVM一般用兩個(gè)臨近32位步長(zhǎng)訪問一個(gè)64位的長(zhǎng)整型變量或一個(gè)64位雙精度浮點(diǎn)型變量。一個(gè)線程可能在完成第一步后等待其它線程執(zhí)行所有的兩步。接下來,第一個(gè)線程可能醒來并完成第二步,產(chǎn)生一個(gè)值既不同于第一個(gè)線程也不同于第二線程的值的變量。結(jié)果,如果至少一個(gè)線程能夠修改一個(gè)長(zhǎng)整型變量或一個(gè)雙精度浮點(diǎn)型變量,那些讀取和(或)修改那個(gè)變量的所有線程就必須使用同步連載訪問。
          主站蜘蛛池模板: 苍南县| 额尔古纳市| 铁力市| 衡东县| 张北县| 天全县| 靖安县| 临汾市| 灵台县| 子长县| 广德县| 邓州市| 苍溪县| 汉沽区| 商都县| 枣阳市| 景泰县| 凌源市| 云安县| 迭部县| 广灵县| 阿拉善盟| 方城县| 宣城市| 正蓝旗| 辽宁省| 昭觉县| 凤庆县| 吉水县| 阜南县| 北安市| 轮台县| 清镇市| 上饶县| 长白| 滨州市| 沐川县| 方正县| 龙川县| 健康| 临海市|