《Java多線程編程實戰指南(設計模式篇)》答疑總結(陸續更新,part1)

《Java多線程編程實戰指南(設計模式篇)》答疑開展以來,不少網友提出的問題既有與本書有關的話題,也有Java多線程編程基礎知識的相關話題。由於時間關係,對於重複的問題我不逐一回復。還請各位網友參考本總結。這裏我將一些與本書相關以及具有代表性的問題提煉下,並附上的我的簡要回復。其實,有些問題的回覆如果要再深入或者詳細,恐怕得寫一篇文章,只是時間關係......

 

活動時間:(11月23日--11月30日)

http://www.iteye.com/topic/1142354 

http://bbs.csdn.net/topics/391863274

 

《Java多線程編程實戰指南(設計模式篇)》中設計模式是如何總計出來的?爲什麼不是更多,或更少?

《Java多線程編程實戰指南(設計模式篇)》作者黃文海回覆:

這些模式是許多人總結出來的。書中收錄的都是我使用和在實際項目中接觸過的。 

所以,有些我瞭解但是沒有具體用過模式,如Leader/Follower,並沒有收錄進來。以後我有了一定的這些模式的使用經驗可能會把它們加進來。 

 

另外,儘管模式具有一定的語言(平臺)中立性。但是,有些模式我認爲在Java平臺中能夠發揮的作用有限,不使用這些模式可能反而使代碼更加簡潔。例如,POSA中收錄的Thread Safe Interface模式,其主要意圖在於在某些不用鎖的情況下(如同一個線程內的組件調用)可以避免鎖的開銷,而在需要鎖的情況下又能使用鎖。在Java中基本上語言本身就支持這樣的效果,且應用開發人員不需要做額外的事情:其一,Java中的鎖是可重入的,這意味着同一個線程多次獲取同一個鎖並不會導致死鎖;其二,Java自從1.6版開始對synchronized關鍵字的執行進行優化(包括偏向鎖Biased Locking、鎖粗化Lock Coarsening和鎖去除Lock Elimination等),這使得非競爭條件下,鎖的消耗大大降低了。也就是說,非競爭的鎖的開銷和無鎖的開銷之間的差距已經縮得很小心。 

 

還有的模式其實是我們已經所熟知,並且也無需在其基礎上做一些其它的動作。運用其它都是直截了當的。比如Patterns In Java中收錄的Single Threaded Execution,其意圖就是使某些代碼在任一時刻只有一個線程能夠執行。大家可能立馬想到synchronized。的確如此。所以,這樣的模式我並沒有收錄。 

 

有沒有一個比較通俗易懂的例子來解釋多線程的概念?

《Java多線程編程實戰指南(設計模式篇)》作者黃文海回覆:

 

銀行的營業廳開一個櫃檯的時候,所有客戶只能被分配到這個櫃檯上。如果同時開幾個櫃檯,那麼所有客戶可以被分配到不同的櫃檯上。這樣,從辦理業務的客戶角度來看,他們等待的時間短了(響應性更好) 。從銀行的角度來看,他們同樣的時間能夠接待的客戶更多了(吞吐率變大了)。 

 

這裏,單個櫃檯可以看做單個線程,它可能導致響應性和吞吐率低;而多個櫃檯可以看做多個線程,它可能使得響應性和吞吐率增加。 

 

正如我前面的帖子提到的,多線程未必就能提高處理效率,所以我在上面用了“可能”。

 

線程的優先級能否保證線程是按照優先級高低的順序運行,使用時需要注意什麼問題?

《Java多線程編程實戰指南(設計模式篇)》作者黃文海回覆:

 

線程優先級我感覺最好把它理解成應用代碼給JVM線程調度器的一個提示信息。它只是JVM線程調度器進行線程調度時的一個參考信息(而不是全部),它並不能保證線程按照優先級高低的順序進行運行。舉個例子來說,假設有3個線程ThreadA、ThreadB和ThreadC,它們的優先級分別是高、普通(中)和低。假設某一個時刻,ThreadA和ThreadB處於I/O等待狀態,而ThreadC處於Runnable狀態,那麼JVM線程調度器此時會選擇ThreadC運行。可見,這裏ThreadA的高優先級並沒有起到任何作用。

 

濫用線程優先級的可能導致線程飢餓(Thread Starvation),即某些線程永遠無法得以運行。這個好比老大同一天給你分配了好幾個任務,當你詢問這個幾個任務的優先級時,他的回答都是”重要“。那麼,你就會困惑:我到底該先完成哪個任務呢?可見,此時所謂的優先級,有等於沒有。

 

因此,一般不建議設置線程的優先級,使用默認值就可以了。

 

什麼是死鎖,如何避免死鎖?

《Java多線程編程實戰指南(設計模式篇)》作者黃文海回覆:

 

死鎖類似於吝嗇鬼落水的故事。一個吝嗇鬼掉進河裏了,有人準備給他施救,但是施救者的也不會游泳,而他的手使勁往河水處伸還是夠不到吝嗇鬼。於是他讓吝嗇鬼把手伸過來(Give me your hand!)。吝嗇鬼一聽到”給”字(Give)就神經緊張,硬是不肯伸手,覺得伸手給別人是吃虧了。於是吝嗇鬼說“不,你伸手給我”(No!Give me your hand!)。於是,一個不能給,一個不肯給,你等我,我等你,救援一事無法進展。

 

死鎖避免有兩種方法:

1、不使用鎖: 例如,Swing和Android都採用這種設計,使得它們的用戶界面組件層使用單線程,也就避免了鎖,自然也就避免了死鎖。詳情可參見《Java多線程編程實戰指南(設計模式篇)》第14章。

2、對鎖的訪問順序進行排序(Lock Ordering)。可參見這篇博文(英文):http://tutorials.jenkov.com/java-concurrency/deadlock-prevention.html

什麼情況下使用多線程,什麼情況下使用單線程?

《Java多線程編程實戰指南(設計模式篇)》作者回復:

 

這實際上是一個收益與成本比的問題。因爲多線程也有自身的開銷和問題,如上下文切換、鎖的開銷以及由鎖可能導致的死鎖等問題,所以使用多線程編程不一定就比使用單線程的處理效率更高。這正如書中所打的一個比方---和尚打水的故事:一個和尚(單線程)挑水喝,兩個和尚(多線程)擔水喝,三個和尚(多線程)沒水喝。可見,多線程的使用可能反而導致處理效率的降低。《Java多線程編程實戰指南(設計模式篇)》第1章對這個問題有講解。如何恰當地使用多線程編程,這點也正是《Java多線程編程實戰指南(設計模式篇)》所能起到的一個作用。 

 

另一方面,在任務原始規模比較大(或者說不小)的情況下,恰當地使用多線程可以提高處理效率。例如,《Java多線程編程實戰指南(設計模式篇)》第13章提到的一個實戰案例:將數據庫中的幾十萬條數據導出到文件中併發送到指定的FTP服務器上。這個實例如果不採用多線程編程,則可能使相應的計算顯得非常慢。 

 

特意地使用單線程編程有時反而可能提高處理效率。這裏,典型的使用場景是程序的處理過程涉及一些獨佔資源或者非線程安全的對象。例如,《Java多線程編程實戰指南(設計模式篇)》第11章的實戰案例:使用非線程安全的FTP客戶端組件將一批本地文件FTP上傳到指定的多個服務器。這個案例中,我們使用了單線程處理FTP文件上傳,以減少多線程相關的開銷。而這個線程的實際處理效率也能滿足我們實際的需要。 

 

Java平臺本身就是個多線程的平臺,Java平臺中線程無處不在:負責Java程序運行的main線程、垃圾回收GC線程、JIT編譯器線程。因此,這裏我們所說的單線程編程實際上是在多線程環境中特意使用單線程。 

 

另外,即使是在單CPU的機器上,多線程編程也是有適用場景的。例如,一個線程正在執行I/O操作(如讀取文件),此時該線程並不佔用CPU(因爲它已經被Switch out了),那麼其它線程,如執行加密/解密計算的線程此時可以佔用CPU執行。這樣,便提高了CPU的利用率,有利於提高系統的吞吐率。

 

如何合理設置線程池的大小(線程數)、使用線程時如何合理控制線程數?

《Java多線程編程實戰指南(設計模式篇)》作者回復:

 

線程池的大小設置一般要考慮到主機的CPU個數(邏輯CPU個數,NCPU)。線程池大小設置過小會導致CPU資源浪費,而設置過大則可能導致消耗過多的內存以及產生更多的上下文切換(導致CPU額外消耗過多)。粗略地說,對於執行CPU密集型的任務(如加密/解密)的線程池其最大大小可以爲2NCPU。對於執行I/O密集型的任務(如寫日誌文件)的線程池其最大大小可以設置爲2NCPU+1。詳情參見《Java多線程編程實戰指南(設計模式篇)》第9章。 

 

Java中我們可以使用Runtime.getRuntime().availableProcessors()來獲取主機的CPU個數。 

 

多個線程同時訪問同一個資源(如變量、文件等)時,如何保證線程安全?

《Java多線程編程實戰指南(設計模式篇)》作者回復:

我們知道,使用鎖(如synchronized和ReetrantLock)是保證線程安全的一個常見方法。這種方法的本質是以互斥的方式保證一個時刻只有一個線程能夠訪問共享變量(資源)。這好比公路維修的時候,原本四車道路在維修路段被弄成了單車道使得車輛只能一輛一輛通行。所以這種方法的缺點非常明顯。《Java多線程編程實戰指南(設計模式篇)》第3章、第10章、第11章介紹的3個模式就可以用來保證線程安全。它們背後的思想是要麼是共享狀態不可變的變量、要麼是不共享變量。

 

文件共享訪問與訪問共享變量是類似的。在Java中我們使用文件時並不是直接對文件進行操作,而是通過Stream和Writer進行。而這些接口本身可能已經對併發訪問進行處理了,即它們本身保證了共享時的線程安全。但是,這只是其中一個方面(下面會講到)。例如,java.io.Writer這個抽象類是我們經常使用的類(如PrintWriter和BufferedWrite)的父類。它在寫文件的時候是加鎖的,即通過鎖去保證線程安全。如java.io.Writer中定義的write方法的代碼所示:

 

 public void write(String str, int off, int len) throws IOException {

        synchronized (lock) {

           //省略其它代碼

        }

  }

  

也就是說上面的write方法是一個原子操作。但是,我們知道“原子操作+原子操作!=原子操作“。所以,如果多個線程使用多個Writer實例寫同一個文件,那麼這個文件的內容就可能紊亂了。當然,按照上面的分析,多個線程用同一個Writer實例寫同一個文件並不會有問題。

 volatile這個關鍵字究竟起到什麼作用(2015.11.27)?

 

《Java多線程編程實戰指南(設計模式篇)》作者回復:

保證賦值操作的原子性。

我們知道對Java中的64位數據類型(long和double)進行賦值的時候,JVM是不保證原子性的。例如:

 

private long count=0;

 

void someUpdate(){

   count=1;

}

 

上述代碼中,一個線程調用 someUpdate更新count值的時候,另外一個線程讀取到的該變量的值可能是0、也可能是1,甚至可能是其它數值。如果對上述變量採用voalitle進行修飾,那麼上述代碼對long型變量的賦值就具有了原子性。因此,其它線程讀取到的該變量的值只可能是0或者1。

保證共享可變變量的可見性。

簡單來說,就是一個線程對一個共享變量的更新對於另外一個線程而言不一定是可見的。比如,

 

private boolean isDone=false;

如果有個線程將上面的變量修改爲true,那麼其它線程可能讀取到的值一直是false。如果將上述變量採用volatile修飾,那麼一個線程將其值修改後,之後有其它線程來讀取該變量的值,後面這個線程總是可以讀取到跟新後的變量值。

禁止重排序。

比如下面的代碼:

private int nonVoaltileVar=1;

private boolean isVarSet=false;

 

private void update(){

  nonVoaltileVar=2;

  isVarSet=true;

}

 

上述代碼執行時,由於重排序的結果,一個線程執行update方法後(假設此時再也沒有其它線程會去更新上述兩個變量的值),其它線程讀取到isVarSet的值爲true的情況下,它所讀取到nonVoaltileVar的值可能仍然是1。這是由於update方法中的兩個語句的執行順序可能被對調(重排序)。而如果我們用voalitle去修飾isVarSet,那麼voaltile會禁止其修飾的變量的賦值操作前的操作被重排序到其之後。這樣,就保證了其它線程讀取到isVarSet的值爲true的情況下,nonVoaltileVar的值總是爲2。

 

《Java多線程編程實戰指南(設計模式篇)》第3章的實戰案例代碼中有使用volatile關鍵字,可以參考下。如果要進一步或者更加詳細的解釋,那要不小的篇幅。深入的理解voaltile關鍵字涉及到CPU訪問內存的機制以及JMM。

 

什麼是Happens-before關係,如何能夠更好地理解它(2015.11.29)?

《Java多線程編程實戰指南(設計模式篇)》作者回復:

 

Happens-before關係是JMM中的一個比較容易誤解的概念。我的理解是它其實是一個形式化(或者模型化)的概念,所以理解起來有些困難。但是,這種比較抽象的概念我們可以對其具體化,通過一些具體的例子來更好的理解它。

 

Happens-before的提出是爲了解決多線程共享變量的可見性問題。我們知道,這個問題編譯器要關心、我們作爲應用開發人員也要關心。理解這點很重要,因爲如果你從編譯器的角度出發去理解Happens-before的概念,就會涉及一些Memory Barrier等與硬件相關的概念。所以,我建議先從應用開發人員的角度出發去理解這個概念,這樣會比較容易。

 

下面,我們對幾個具體的Happens-before規則從應用開發人員的角度進行“解讀”,通過這個解讀相信大家都能明白Happens-before是個什麼東西,至少明白它對我們(應用開發人員)意味着什麼。

 

線程啓動規則。對一個線程進行的Thread.start調用happens‐before被啓動的線程中的每個動作(Action)。

Thread start rule. A call to Thread.start on a thread happens‐before every action in the started thread.

 

所謂動作(Action),包括讀變量、寫變量、啓動線程、等待線程停止(join)和鎖的獲取與釋放。

 

上面的描述乍看起來很抽象,也顯得像廢話——因爲線程只有在啓動以後,相應線程的run方法中的代碼纔會被執行。所以,線程肯定是啓動在先,運行在後。這大家都知道啊!不過,這裏happens‐before要說明並不是我們剛纔將的時間上先後關係。它要描述的是某種可見性的保證。以上面的規則爲例,這個規則意味着父線程在啓動一個子線程之前對任何一個變量的變更對於這個子線程而言都是可見的(這纔是我們關心的話題!)。例如:

 

public class HappensBefore {

static int a;

static long b;

 

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

Thread childThread = new Thread() {

@Override

public void run() {

 

if (a == 1) {

System.out.println(b);

 

b = 900L;

a = 3;

}

}

};

 

a = 1;

b = 10000L;

a = 2;

b = 1L;

 

childThread.join();

 

System.out.println("a=" + a + ",b=" + b);

}

 

}

 

上述代碼的輸出爲:

 

10000

a=3,b=900

 

這是因爲,根據上面的對“線程啓動規則”的解讀,子線程childThread始終是可以看到其父線程(main線程)在啓動其前對變量的寫操作的結果(即a==1,b == 10000)。因此,childThread的run方法運行的時候看到a的值爲1以及b的值爲10000是有保證的。

 

再看另外一個具體的Happens-before的規則:

 

線程終止規則。一個線程中的任何一個動作都 happens‐before檢測該線程終止的線程中的任何一個動作。這包括檢測線程調用被檢測線程的Thread.join或者Thread.isAlive。

Thread  termination  rule.  Any  action  in  a  thread  happens‐before  any  other  thread  detects  that  thread  has 

terminated, either by successfully return from Thread.join or by Thread.isAlive returning false. 

 

同樣,這個規則的理解關鍵還在於可見性方面它對我們(應用開發人員)意味着什麼。這條規則說明,當一個線程終止的時候,該線程所做的所有變量更新動作的結果對於等待其停止的線程而言都是可見的(當然,要等Thread.join/Thread.isAlive調用返回)。

 

還是以上面的代碼爲例,根據上面對“線程終止規則”的解讀,當子線程終止的時候,調用其join方法的父線程(main線程)看到該線程更新過的變量值,即變量a的值爲3,變量b的值爲900,是有保證的。

 

此時,我們對Happens-before有了一定的認識。這時,可以考慮從編譯器的角度(假設我們是編譯器開發人員),去理解Happens-before這個概念了。

 

我們知道Thread.start這個方法是一個synchronized修飾的方法。上述“線程啓動規則”的實現就是通過編譯器對synchronized關鍵字的實現而實現的。編譯器會在synchronized塊進入和退出的時候分別插入恰當的Memory Barrier(指令)。這些指令的作用是保證一個線程對變量的更新得以刷新到主內存(而不是寄存器、寫緩衝器等“工作內存”)中,並防止一些指令重排序。

 

接着,我們可以循着上述方法再去解讀其它Happens-before規則,使得我們對Happens-before的理解更加深刻。

什麼是ThreadLocal類,哪些場景下可以使用ThreadLocal類(2015-12-01)?

《Java多線程編程實戰指南(設計模式篇)》作者回復:

 

ThreadLocal類是個什麼東西的確不容易解釋。要深入理解ThreadLocal類,還是得從爲什麼有這個類說起。

 

打個比方說。兩個小孩玩一臺遙控小汽車玩具,一個時刻只能有一個小孩操控遙控器,另外一個小孩只能等待,弄不好兩個小孩還會爲搶遙控器的控制權而打架!因此,共享是好的,但是有時也會產生一些問題。於是,我們容易想到一個解決由共享導致的麻煩,那就是不共享——給兩個小孩給咱買一臺同一型號的遙控小汽車,讓它們各自玩各自的!

 

回到多線程編程領域,多線程編程中共享變量(數據)往往導致要加鎖,而鎖又會導致等待以及上下文切換、死鎖等開銷和問題。因此,有時候不共享是最好的。這就是引入ThreadLocal的原因。

 

多數情況下,我們訪問一個變量值是通過使用相應的變量名進行的。我們可以把ThreadLocal類的一個實例看做變量名,通過這個變量名我們可以獲得一個變量值,這個變量值同時還與具體的線程相關聯。也就是說,特定線程與特定這樣的變量名的組合決定了一個特定的變量值。也就是說,假設Java中有這樣一個關鍵字 thread_specific,它可以用來修飾某個變量。這樣的變量一旦被多個線程訪問,各個線程所得到的變量值總是屬於該線程所特有的那一份,彼此之間互不干擾。這個假設的關鍵字所起到的作用正是ThreadLocal類所要實現的效果。

 

private thread_specific SimpleDateFormat threadSpecificSdf=new SimpleDateFormat("MMddHHmmss");

 

形象地說ThreadLocal類可以這樣理解:每個線程都持有一個其特有(私有)的一個儲物櫃。一個儲物櫃可以有多個儲物箱,每個儲物箱中存放的東西就是變量值。每個線程只能訪問自己的儲物櫃而不能訪問別的線程的擁有儲物櫃。並且,每個儲物箱都有一把鑰匙(Key),一把鑰匙只能開一個儲物箱。一把鑰匙就是一個ThreadLocal實例。因此,我們就可以看到下面的這種決定關係:

 

{線程對象(儲物櫃),ThreadLocal實例(儲物箱鑰匙)}→變量值(儲物箱中存放的東西)

 

例如,

{thread1,threadLocalA}→String1

 

{thread1,threadLocalB}→String2

 

{thread2,threadLocalC}→String3

 

{thread2,threadLocalD}→String4

 

 

《Java多線程編程實戰指南(設計模式篇)》第10章講解的設計模式的實現使用了ThreadLocal類。這一章的“模式評價與實現考量”一節總結了ThreadLocal類的4種典型應用場景。書中有詳細的結束和示例代碼,這裏我簡單列舉下。

 

場景一:需要使用非線程安全對象,但是又不希望引入鎖。

      這個典型的例子就是在多線程環境中在不加鎖的情況下保證對SimpleDateFormat類使用的線程安全。如下代碼所示:

 

public class SimpleDateFormatExample {

// 注意這裏!SimpleDateFormat是非線程安全,這意味着直接在多個線程間共享它是有問題的。

private static ThreadLocal<SimpleDateFormat> tlSdf = new ThreadLocal<SimpleDateFormat>() {

protected SimpleDateFormat initialValue() {

return new SimpleDateFormat("MMddHHmmss");

};

};

 

public void someOper(Date date) {

String ts = tlSdf.get().format(date);

System.out.println(ts);

}

 

}

 

場景二:需要使用線程安全對象,但是希望避免其使用的鎖的開銷和相關問題。

      比如,隨機數生成器類Random是個線程安全的對象,這是因爲它內部使用鎖。雖然我們可以在多個線程間共享Random實例而不會導致線程安全問題,但是這涉及鎖的開銷。如果想避免這種開銷,那麼一個好的方法是每個線程只使用一個Random實例來生成隨機數。在JDK7中引入的類java.util.concurrent.ThreadLocalRandom體現的正是這種思想。《Java多線程編程實戰指南(設計模式篇)》第10章所舉的實戰案例就是這種應用場景,大家可以參考下。

場景三:隱式參數傳遞。

      一個ThreadLocal類的實例可以被同一個線程內的不同方法(可以跨類)使用。具體實現通常藉助單例(Singleton)模式。

場景四:特定於線程的單例(Singleton)模式。

      傳統的單例模式實際上是保證對於某個類,一個JVM下的一個ClassLoader下最多隻有一個實例。而藉助ThreadLocal我們可以實現對於某個類,每個線程可以擁有該類最多一個實例。

 

ThreadLocal類使用時需要特別注意以下兩點:

1、ThreadLocal類的實例通常設置爲某個類的靜態變量

  即通常的使用格式是:

  private static ThreadLocal<XXX> tlVar=new ThreadLocal<XXX>() {

protected XXX initialValue() {

return new XXX();

};

};

 

 

 這是因爲:一個ThreadLocal實例就對應一個線程特有的變量值,如果把ThreadLocal作爲某個類的實例變量,由於一個類可以有多個實例,那麼就會有多個ThreadLocal實例被創建。即便是對於一個線程而言,這多個ThreadLocal實例就對應了多個該線程特有的變量值。而這通常不是我們所需要。如果我們需要爲同一線程創建不同的該線程特有的變量值,那應該創建不同名字的ThreadLocal實例。例如:

    private static ThreadLocal<XXX> tlVarA=new ThreadLocal<XXX>() {

protected XXX initialValue() {

return new XXX();

};

};

 

private static ThreadLocal<YYY> tlVarB=new ThreadLocal<YYY>() {

protected YYY initialValue() {

return new YYY();

};

};

 

2、在線程池環境下(如Web應用服務器下),使用ThreadLocal可能導致內存泄漏

  這種內存泄漏的原因分析可以從Class(也是一個對象)及負責加載其的ClassLoader之間的關係、JDK對ThreadLocal的具體實現以及Web應用服務器加載Web應用程序的原理入手。分析起來需要花費不是篇幅,《Java多線程編程實戰指南(設計模式篇)》10章有詳細的分析和配圖。

在此基礎上,我們可以給出相應的解決方案。詳情參考《Java多線程編程實戰指南(設計模式篇)》10章。

 

學習Java多線程編程/併發編程有哪些建議?

《Java多線程編程實戰指南(設計模式篇)》作者回復:

你的問題提的很好,我個人也是比較注重的學習方法的。這點,我也儘可能地體現在我的書中。

 

我認爲學一樣東西,要從把握它的基本概念和原理入手。並在這個過程中注意概念和概念之間的關係,知其然而知其所以然,並主動去思考一些問題。當然,“覺知此事需躬行”,自己動手去實踐是少不了的。

 

比如拿你上面的描述中涉及的幾個概念來說。“互斥”,站在它背後的是共享可變數據(Shared Mutable Data/Variable),也就是說的出現或者之所以需要它完全是因爲我們在多個線程間共享了可以改變的數據。換句話說,如果多個線程之間不共享數據(參見《Java多線程編程實戰指南(設計模式篇)》第10章)或者共享的數據是不可變的(參見《Java多線程編程實戰指南(設計模式篇)》第3章),那麼我們就無需互斥,程序的計算效率也就提高了。因此,多線程並不一定就意味着“互斥”。互斥的結果是某些代碼在任意一個時刻只有一個線程能夠執行,那麼我們可以思考下面這樣一個問題:

 

synchronized塊可以實現互斥,那麼synchronized塊保護的是臨界區代碼麼?

 

這個問題如果回答“是”,我認爲也沒有錯。但是,如果深入一步理解,我們會發現synchronized塊正在要“保護”的是臨界區代碼所訪問的共享可變變量,而不是代碼本身。

 

在比如說“同步”(同步機制),爲什麼需要同步呢?一方面是要用它來“保護”共享可變數據。另外,也是通過它來保證多線程間共享的數據(不一定是可變的)之間的內存可見性(Visibility);並且,禁止指令重排序也是通過同步機制實現的。這裏,又涉及了一個概念“”內存可見性“,站在它背後的是CPU通過緩存(Cache)卻訪問內存以提高其處理效率這個事實以及JIT編譯器處於對代碼執行效率的考慮可能對代碼作出的優化。同樣,指令重排序是個什麼概念,什麼情況下我們需要禁止(或者阻止)指令重排序也是我們要掌握的概念和原理。

 

再比如說,我們都知道Java中有兩種方法可以創建線程,那麼這兩種方法有什麼區別呢?當我們掌握了線程安全這一概念以後,思考這個問題的答案就有了方向了。

 

我們經常說“多線程編程”、“併發編程”,往往也不對二者進行區分。那麼,二者有什麼聯繫和區別呢?

 

線程(多線程)其實只是一種併發編程的模型,也就是說多線程可以實現併發編程。而併發編程卻不一定就是多線程編程,有其它的方法,如Actor模型也能實現併發編程。當然,線程模型可以是其它併發編程模型的基礎。這樣把概念弄清楚,有助於我們擴展思路,開闊眼界。例如,《Java多線程編程實戰指南(設計模式篇)》第8章介紹的Active Object模式其背後的思想和Actor模型非常相似。

 

反過來說,不能真正掌握基本概念和原理,就會導致表面上我們是學會了某些東西,但是在實際的工作過程中一遇到問題以自己的力量(不問別人、不搜索)就搞不定了。這好比駕校老師教學生如何發動小汽車、如何變道、如何換擋等等,學生也能自己操作起來。但是,學生考到駕照後,自己上路的時候就會遇到許多實際的問題,比如車子熄火了怎麼辦?路過積水的涵洞怎麼辦?這些自己能搞定麼?再拿我們工作中的實際例子來說,如果我們不能理解到JSP就是一個Servlet的這個事實,不知道JSP經歷從翻譯、編譯到運行的這樣一個處理過程,那麼在遇到JSP問題的時候我們自己可能就搞不定,比如一個JSP中include了另外一個JSP,被include的JSP內容更新了,而主JSP在運行的時候卻始終沒有出現更新後的效果。這樣的問題,沒有本質上把握JSP的概念和處理原理,僅憑自己是很難搞定的。

 

問題人人都會遇到,區別是老手能夠很快找到問題所在,並給出簡單有效的解決方法,而新手可能一直在原地踏步,甚至走進死衚衕。究其原因,經驗差別固然是一方面,我上面所講應該也是重要的一方面。

 

至於大數據、雲計算,我在工作過程中並沒有接觸。但是,我想方法是相似的。如果是我學它們,我會從這些技術或者理念的出現是要解決什麼問題以及它們的一些基本概念和原理入手。另外,如果時間上允許的話,我建議是先學一樣技術,深入的學習並掌握它,再次基礎上再去學習其它知識、技術。這樣,對於後學的東西而言,我們可以充分利用之前學習到知識、經驗以及積累的能力,可以使學習後學的東西輕鬆一點。這種現象心理學上稱之爲”遷移“,類似我們所說的”觸類旁通“。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章