免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
Java 線程/內(nèi)存模型的缺陷和增強(qiáng)
Java 線程/內(nèi)存模型的缺陷和增強(qiáng)
2007年01月10日 星期三 11:41

Java在語言層次上實(shí)現(xiàn)了對(duì)線程的支持。它提供了Thread/Runnable/ThreadGroup等一系列封裝的類和接口,讓程序員可以高效的開發(fā)Java多線程應(yīng)用。為了實(shí)現(xiàn)同步,Java提供了synchronize關(guān)鍵字以及object的wait()/notify()機(jī)制,可是在簡(jiǎn)單易用的背后,應(yīng)藏著更為復(fù)雜的玄機(jī),很多問題就是由此而起。

  一、Java內(nèi)存模型

  在了解Java的同步秘密之前,先來看看JMM(Java Memory Model)。

  Java被設(shè)計(jì)為跨平臺(tái)的語言,在內(nèi)存管理上,顯然也要有一個(gè)統(tǒng)一的模型。而且Java語言最大的特點(diǎn)就是廢除了指針,把程序員從痛苦中解脫出來,不用再考慮內(nèi)存使用和管理方面的問題。
可惜世事總不盡如人意,雖然JMM設(shè)計(jì)上方便了程序員,但是它增加了虛擬機(jī)的復(fù)雜程度,而且還導(dǎo)致某些編程技巧在Java語言中失效。

  JMM主要是為了規(guī)定了線程和內(nèi)存之間的一些關(guān)系。對(duì)Java程序員來說只需負(fù)責(zé)用synchronized同步關(guān)鍵字,其它諸如與線程/內(nèi)存之間進(jìn)行數(shù)據(jù)交換/同步等繁瑣工作均由虛擬機(jī)負(fù)責(zé)完成。如圖1所示:根據(jù)JMM的設(shè)計(jì),系統(tǒng)存在一個(gè)主內(nèi)存(Main Memory),Java中所有變量都儲(chǔ)存在主存中,對(duì)于所有線程都是共享的。每條線程都有自己的工作內(nèi)存(Working Memory),工作內(nèi)存中保存的是主存中某些變量的拷貝,線程對(duì)所有變量的操作都是在工作內(nèi)存中進(jìn)行,線程之間無法相互直接訪問,變量傳遞均需要通過主存完成。


圖1 Java內(nèi)存模型示例圖

  線程若要對(duì)某變量進(jìn)行操作,必須經(jīng)過一系列步驟:首先從主存復(fù)制/刷新數(shù)據(jù)到工作內(nèi)存,然后執(zhí)行代碼,進(jìn)行引用/賦值操作,最后把變量?jī)?nèi)容寫回Main Memory。Java語言規(guī)范(JLS)中對(duì)線程和主存互操作定義了6個(gè)行為,分別為load,save,read,write,assign和use,這些操作行為具有原子性,且相互依賴,有明確的調(diào)用先后順序。具體的描述請(qǐng)參見JLS第17章。

  我們?cè)谇懊娴恼鹿?jié)介紹了synchronized的作用,現(xiàn)在,從JMM的角度來重新審視synchronized關(guān)鍵字。

  假設(shè)某條線程執(zhí)行一個(gè)synchronized代碼段,其間對(duì)某變量進(jìn)行操作,JVM會(huì)依次執(zhí)行如下動(dòng)作:

  (1) 獲取同步對(duì)象monitor (lock)

  (2) 從主存復(fù)制變量到當(dāng)前工作內(nèi)存 (read and load)

  (3) 執(zhí)行代碼,改變共享變量值 (use and assign)

  (4) 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容 (store and write)

  (5) 釋放同步對(duì)象鎖 (unlock)

  可見,synchronized的另外一個(gè)作用是保證主存內(nèi)容和線程的工作內(nèi)存中的數(shù)據(jù)的一致性。如果沒有使用synchronized關(guān)鍵字,JVM不保證第2步和第4步會(huì)嚴(yán)格按照上述次序立即執(zhí)行。因?yàn)楦鶕?jù)JLS中的規(guī)定,線程的工作內(nèi)存和主存之間的數(shù)據(jù)交換是松耦合的,什么時(shí)候需要刷新工作內(nèi)存或者更新主內(nèi)存內(nèi)容,可以由具體的虛擬機(jī)實(shí)現(xiàn)自行決定。如果多個(gè)線程同時(shí)執(zhí)行一段未經(jīng)synchronized保護(hù)的代碼段,很有可能某條線程已經(jīng)改動(dòng)了變量的值,但是其他線程卻無法看到這個(gè)改動(dòng),依然在舊的變量值上進(jìn)行運(yùn)算,最終導(dǎo)致不可預(yù)料的運(yùn)算結(jié)果。

二、DCL失效

  這一節(jié)我們要討論的是一個(gè)讓Java丟臉的話題:DCL失效。在開始討論之前,先介紹一下LazyLoad,這種技巧很常用,就是指一個(gè)類包含某個(gè)成員變量,在類初始化的時(shí)候并不立即為該變量初始化一個(gè)實(shí)例,而是等到真正要使用到該變量的時(shí)候才初始化之。

  例如下面的代碼:

  代碼1

class Foo 
{
 private Resource res = null;
 public Resource getResource() 
 {
  if (res == null) res = new Resource(); 
  return res;
 }
}

  由于LazyLoad可以有效的減少系統(tǒng)資源消耗,提高程序整體的性能,所以被廣泛的使用,連Java的缺省類加載器也采用這種方法來加載Java類。

  在單線程環(huán)境下,一切都相安無事,但如果把上面的代碼放到多線程環(huán)境下運(yùn)行,那么就可能會(huì)出現(xiàn)問題。假設(shè)有2條線程,同時(shí)執(zhí)行到了if(res == null),那么很有可能res被初始化2次,為了避免這樣的Race Condition,得用synchronized關(guān)鍵字把上面的方法同步起來。代碼如下:

  代碼2

Class Foo 
{
 Private Resource res = null;
 Public synchronized Resource getResource()
 {
  If (res == null) res = new Resource();
  return res;
 }
}

  現(xiàn)在Race Condition解決了,一切都很好。

  N天過后,好學(xué)的你偶然看了一本Refactoring的魔書,深深為之打動(dòng),準(zhǔn)備自己嘗試這重構(gòu)一些以前寫過的程序,于是找到了上面這段代碼。你已經(jīng)不再是以前的Java菜鳥,深知synchronized過的方法在速度上要比未同步的方法慢上100倍,同時(shí)你也發(fā)現(xiàn),只有第一次調(diào)用該方法的時(shí)候才需要同步,而一旦res初始化完成,同步完全沒必要。所以你很快就把代碼重構(gòu)成了下面的樣子:

  代碼3

Class Foo 
{
 Private Resource res = null;
 Public Resource getResource() 
 {
  If (res == null)
  {
   synchronized(this)
   {
    if(res == null)
    {
     res = new Resource();
    }
   }
  }
  return res;
 }
}

  這種看起來很完美的優(yōu)化技巧就是Double-Checked Locking。但是很遺憾,根據(jù)Java的語言規(guī)范,上面的代碼是不可靠的。
造成DCL失效的原因之一是編譯器的優(yōu)化會(huì)調(diào)整代碼的次序。只要是在單個(gè)線程情況下執(zhí)行結(jié)果是正確的,就可以認(rèn)為編譯器這樣的“自作主張的調(diào)整代碼次序”的行為是合法的。JLS在某些方面的規(guī)定比較自由,就是為了讓JVM有更多余地進(jìn)行代碼優(yōu)化以提高執(zhí)行效率。而現(xiàn)在的CPU大多使用超流水線技術(shù)來加快代碼執(zhí)行速度,針對(duì)這樣的CPU,編譯器采取的代碼優(yōu)化的方法之一就是在調(diào)整某些代碼的次序,盡可能保證在程序執(zhí)行的時(shí)候不要讓CPU的指令流水線斷流,從而提高程序的執(zhí)行速度。正是這樣的代碼調(diào)整會(huì)導(dǎo)致DCL的失效。為了進(jìn)一步證明這個(gè)問題,引用一下《DCL Broken Declaration》文章中的例子:

  設(shè)一行Java代碼:

Objects[i].reference = new Object();

  經(jīng)過Symantec JIT編譯器編譯過以后,最終會(huì)變成如下匯編碼在機(jī)器中執(zhí)行:

0206106A mov  eax,0F97E78h
0206106F call 01F6B210       ;為Object申請(qǐng)內(nèi)存空間
                  ; 返回值放在eax中
02061074 mov  dword ptr [ebp],eax  ; EBP 中是objects[i].reference的地址
                  ; 將返回的空間地址放入其中
                  ; 此時(shí)Object尚未初始化
02061077 mov  ecx,dword ptr [eax]   ; dereference eax所指向的內(nèi)容
                   ; 獲得新創(chuàng)建對(duì)象的起始地址
02061079 mov  dword ptr [ecx],100h   ; 下面4行是內(nèi)聯(lián)的構(gòu)造函數(shù)
0206107F mov  dword ptr [ecx+4],200h 
02061086 mov  dword ptr [ecx+8],400h
0206108D mov  dword ptr [ecx+0Ch],0F84030h

  可見,Object構(gòu)造函數(shù)尚未調(diào)用,但是已經(jīng)能夠通過objects[i].reference獲得Object對(duì)象實(shí)例的引用。

  如果把代碼放到多線程環(huán)境下運(yùn)行,某線程在執(zhí)行到該行代碼的時(shí)候JVM或者操作系統(tǒng)進(jìn)行了一次線程切換,其他線程顯然會(huì)發(fā)現(xiàn)msg對(duì)象已經(jīng)不為空,導(dǎo)致Lazy load的判斷語句if(objects[i].reference == null)不成立。線程認(rèn)為對(duì)象已經(jīng)建立成功,隨之可能會(huì)使用對(duì)象的成員變量或者調(diào)用該對(duì)象實(shí)例的方法,最終導(dǎo)致不可預(yù)測(cè)的錯(cuò)誤。

  原因之二是在共享內(nèi)存的SMP機(jī)上,每個(gè)CPU有自己的Cache和寄存器,共享同一個(gè)系統(tǒng)內(nèi)存。所以CPU可能會(huì)動(dòng)態(tài)調(diào)整指令的執(zhí)行次序,以更好的進(jìn)行并行運(yùn)算并且把運(yùn)算結(jié)果與主內(nèi)存同步。這樣的代碼次序調(diào)整也可能導(dǎo)致DCL失效?;叵胍幌虑懊鎸?duì)Java內(nèi)存模型的介紹,我們這里可以把Main Memory看作系統(tǒng)的物理內(nèi)存,把Thread Working Memory認(rèn)為是CPU內(nèi)部的Cache和寄存器,沒有synchronized的保護(hù),Cache和寄存器的內(nèi)容就不會(huì)及時(shí)和主內(nèi)存的內(nèi)容同步,從而導(dǎo)致一條線程無法看到另一條線程對(duì)一些變量的改動(dòng)。
結(jié)合代碼3來舉例說明,假設(shè)Resource類的實(shí)現(xiàn)如下:

Class Resource{ Object obj;}

  即Resource類有一個(gè)obj成員變量引用了Object的一個(gè)實(shí)例。假設(shè)2條線程在運(yùn)行,其狀態(tài)用如下簡(jiǎn)化圖表示:


圖2 

  現(xiàn)在Thread-1構(gòu)造了Resource實(shí)例,初始化過程中改動(dòng)了obj的一些內(nèi)容。退出同步代碼段后,因?yàn)椴扇×送綑C(jī)制,Thread-1所做的改動(dòng)都會(huì)反映到主存中。接下來Thread-2獲得了新的Resource實(shí)例變量res,由于沒有使用synchronized保護(hù)所以Thread-2不會(huì)進(jìn)行刷新工作內(nèi)存的操作。假如之前Thread-2的工作內(nèi)存中已經(jīng)有了obj實(shí)例的一份拷貝,那么Thread-2在對(duì)obj執(zhí)行use操作的時(shí)候就不會(huì)去執(zhí)行l(wèi)oad操作,這樣一來就無法看到Thread-1對(duì)obj的改變,這顯然會(huì)導(dǎo)致錯(cuò)誤的運(yùn)算結(jié)果。此外,Thread-1在退出同步代碼段的時(shí)刻對(duì)ref和obj執(zhí)行的寫入主存的操作次序也是不確定的,所以即使Thread-2對(duì)obj執(zhí)行了load操作,也有可能只讀到obj的初試狀態(tài)的數(shù)據(jù)。(注:這里的load/use均指JMM定義的操作)

  有很多人不死心,試圖想出了很多精妙的辦法來解決這個(gè)問題,但最終都失敗了。事實(shí)上,無論是目前的JMM還是已經(jīng)作為JSR提交的JMM模型的增強(qiáng),DCL都不能正常使用。在William Pugh的論文《Fixing the Java Memory Model》中詳細(xì)的探討了JMM的一些硬傷,更嘗試給出一個(gè)新的內(nèi)存模型,有興趣深入研究的讀者可以參見文后的參考資料。

  如果你設(shè)計(jì)的對(duì)象在程序中只有一個(gè)實(shí)例,即singleton的,有一種可行的解決辦法來實(shí)現(xiàn)其LazyLoad:就是利用類加載器的LazyLoad特性。代碼如下:

Class ResSingleton {public static Resource res = new Resource();}

  這里ResSingleton只有一個(gè)靜態(tài)成員變量。當(dāng)?shù)谝淮问褂肦esSingleton.res的時(shí)候,JVM才會(huì)初始化一個(gè)Resource實(shí)例,并且JVM會(huì)保證初始化的結(jié)果及時(shí)寫入主存,能讓其他線程看到,這樣就成功的實(shí)現(xiàn)了LazyLoad。

  除了這個(gè)辦法以外,還可以使用ThreadLocal來實(shí)現(xiàn)DCL的方法,但是由于ThreadLocal的實(shí)現(xiàn)效率比較低,所以這種解決辦法會(huì)有較大的性能損失,有興趣的讀者可以參考文后的參考資料。

  最后要說明的是,對(duì)于DCL是否有效,個(gè)人認(rèn)為更多的是一種帶有學(xué)究氣的推斷和討論。而從純理論的角度來看,存取任何可能共享的變量(對(duì)象引用)都需要同步保護(hù),否則都有可能出錯(cuò),但是處處用synchronized又會(huì)增加死鎖的發(fā)生幾率,苦命的程序員怎么來解決這個(gè)矛盾呢?事實(shí)上,在很多Java開源項(xiàng)目(比如Ofbiz/Jive等)的代碼中都能找到使用DCL的證據(jù),我在具體的實(shí)踐中也沒有碰到過因DCL而發(fā)生的程序異常。個(gè)人的偏好是:不妨先大膽使用DCL,等出現(xiàn)問題再用synchronized逐步排除之。也許有人偏于保守,認(rèn)為穩(wěn)定壓倒一切,那就不妨先用synchronized同步起來,我想這是一個(gè)見仁見智的問題,而且得針對(duì)具體的項(xiàng)目具體分析后才能決定。還有一個(gè)辦法就是寫一個(gè)測(cè)試案例來測(cè)試一下系統(tǒng)是否存在DCL現(xiàn)象,附帶的光盤中提供了這樣一個(gè)例子,感興趣的讀者可以自行編譯測(cè)試。不管結(jié)果怎樣,這樣的討論有助于我們更好的認(rèn)識(shí)JMM,養(yǎng)成用多線程的思路去分析問題的習(xí)慣,提高我們的程序設(shè)計(jì)能力。
 三、Java線程同步增強(qiáng)包

  相信你已經(jīng)了解了Java用于同步的3板斧:synchronized/wait/notify,它們的確簡(jiǎn)單而有效。但是在某些情況下,我們需要更加復(fù)雜的同步工具。有些簡(jiǎn)單的同步工具類,諸如ThreadBarrier,Semaphore,ReadWriteLock等,可以自己編程實(shí)現(xiàn)?,F(xiàn)在要介紹的是牛人Doug Lea的Concurrent包。這個(gè)包專門為實(shí)現(xiàn)Java高級(jí)并行程序所開發(fā),可以滿足我們絕大部分的要求。更令人興奮的是,這個(gè)包公開源代碼,可自由下載。且在JDK1.5中該包將作為SDK一部分提供給Java開發(fā)人員。

  Concurrent Package提供了一系列基本的操作接口,包括sync,channel,executor,barrier,callable等。這里將對(duì)前三種接口及其部分派生類進(jìn)行簡(jiǎn)單的介紹。

  sync接口:專門負(fù)責(zé)同步操作,用于替代Java提供的synchronized關(guān)鍵字,以實(shí)現(xiàn)更加靈活的代碼同步。其類關(guān)系圖如下:


圖3 Concurrent包Sync接口類關(guān)系圖

  Semaphore:和前面介紹的代碼類似,可用于pool類實(shí)現(xiàn)資源管理限制。提供了acquire()方法允許在設(shè)定時(shí)間內(nèi)嘗試鎖定信號(hào)量,若超時(shí)則返回false。

  Mutex:和Java的synchronized類似,與之不同的是,synchronized的同步段只能限制在一個(gè)方法內(nèi),而Mutex對(duì)象可以作為參數(shù)在方法間傳遞,所以可以把同步代碼范圍擴(kuò)大到跨方法甚至跨對(duì)象。

  NullSync:一個(gè)比較奇怪的東西,其方法的內(nèi)部實(shí)現(xiàn)都是空的,可能是作者認(rèn)為如果你在實(shí)際中發(fā)現(xiàn)某段代碼根本可以不用同步,但是又不想過多改動(dòng)這段代碼,那么就可以用NullSync來替代原來的Sync實(shí)例。此外,由于NullSync的方法都是synchronized,所以還是保留了“內(nèi)存壁壘”的特性。

  ObservableSync:把sync和observer模式結(jié)合起來,當(dāng)sync的方法被調(diào)用時(shí),把消息通知給訂閱者,可用于同步性能調(diào)試。

  TimeoutSync:可以認(rèn)為是一個(gè)adaptor,其構(gòu)造函數(shù)如下:

public TimeoutSync(Sync sync, long timeout){…}

  具體上鎖的代碼靠構(gòu)造函數(shù)傳入的sync實(shí)例來完成,其自身只負(fù)責(zé)監(jiān)測(cè)上鎖操作是否超時(shí),可與SyncSet合用。

  Channel接口:代表一種具備同步控制能力的容器,你可以從中存放/讀取對(duì)象。不同于JDK中的Collection接口,可以把Channel看作是連接對(duì)象構(gòu)造者(Producer)和對(duì)象使用者(Consumer)之間的一根管道。如圖所示:


圖4 Concurrent包Channel接口示意圖

  通過和Sync接口配合,Channel提供了阻塞式的對(duì)象存取方法(put/take)以及可設(shè)置阻塞等待時(shí)間的offer/poll方法。實(shí)現(xiàn)Channel接口的類有LinkedQueue,BoundedLinkedQueue,BoundedBuffer,BoundedPriorityQueue,SynchronousChannel,Slot等。


圖5 Concurrent包Channel接口部分類關(guān)系圖

  使用Channel我們可以很容易的編寫具備消息隊(duì)列功能的代碼,示例如下:

  代碼4

Package org.javaresearch.j2seimproved.thread;

Import EDU.oswego.cs.dl.util.concurrent.*;

public class TestChannel {
 final Channel msgQ = new LinkedQueue(); //log信息隊(duì)列

 public static void main(String[] args) {
  TestChannel tc = new TestChannel();
  For(int i = 0;i < 10;i ++){
   Try{
    tc.serve();
    Thread.sleep(1000);
   }catch(InterruptedException ie){
  }
 }
}

public void serve() throws InterruptedException {
 String status = doService();
 //把doService()返回狀態(tài)放入Channel,后臺(tái)logger線程自動(dòng)讀取之
 msgQ.put(status); 
}

private String doService() {
 // Do service here
 return "service completed OK! ";
}

public TestChannel() { // start background thread
 Runnable logger = new Runnable() {
 public void run() {
  try {
   for (; ; )
   System.out.println("Logger: " + msgQ.take());
  }
  catch (InterruptedException ie) {}
 }
};
new Thread(logger).start();
}
}

   Excutor/ThreadFactory接口: 把相關(guān)的線程創(chuàng)建/回收/維護(hù)/調(diào)度等工作封裝起來,而讓調(diào)用者只專心于具體任務(wù)的編碼工作(即實(shí)現(xiàn)Runnable接口),不必顯式創(chuàng)建Thread類實(shí)例就能異步執(zhí)行任務(wù)。
使用Executor還有一個(gè)好處,就是實(shí)現(xiàn)線程的“輕量級(jí)”使用。前面章節(jié)曾提到,即使我們實(shí)現(xiàn)了Runnable接口,要真正的創(chuàng)建線程,還是得通過new Thread()來完成,在這種情況下,Runnable對(duì)象(任務(wù))和Thread對(duì)象(線程)是1對(duì)1的關(guān)系。如果任務(wù)多而簡(jiǎn)單,完全可以給每條線程配備一個(gè)任務(wù)隊(duì)列,讓Runnable對(duì)象(任務(wù))和Executor對(duì)象變成n:1的關(guān)系。使用了Executor,我們可以把上面兩種線程策略都封裝到具體的Executor實(shí)現(xiàn)中,方便代碼的實(shí)現(xiàn)和維護(hù)。

  具體的實(shí)現(xiàn)有: PooledExecutor,ThreadedExecutor,QueuedExecutor,F(xiàn)JTaskRunnerGroup等
類關(guān)系圖如下:


圖6 Concurrent包Executor/ThreadFactory接口部分類關(guān)系圖

  下面給出一段代碼,使用PooledExecutor實(shí)現(xiàn)一個(gè)簡(jiǎn)單的多線程服務(wù)器

  代碼5

package org.javaresearch.j2seimproved.thread;
import java.net.*;
import EDU.oswego.cs.dl.util.concurrent.*;
public class TestExecutor
{
 public static void main(String[] args) 
 {
  PooledExecutor pool = new PooledExecutor(new BoundedBuffer(10), 20); 
  pool.createThreads(4); 
  try 
  {
   ServerSocket socket = new ServerSocket(9999);
   for (; ; ) 
   {
    final Socket connection = socket.accept();
    pool.execute(new Runnable() 
    {
     public void run() 
     {
      new Handler().process(connection);
     }
    }); 
   }
  }
  catch (Exception e) {} 
  // die 
 }
 static class Handler { void process(Socket s){ } }
}

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
學(xué)習(xí)Java多線程之volatile域
Java基礎(chǔ)知識(shí)總結(jié) - 超詳細(xì)篇收藏
java并發(fā)(3):并發(fā)工具集
Java Threads 多線程10分鐘參考手冊(cè)
Java初級(jí)工程師面試題精選3
聊聊我對(duì)Java內(nèi)存模型的理解 | 并發(fā)編程網(wǎng)
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服