[分享]Java多執行緒程式設計

Java 課程討論區

版主: XO, 蔡明志, rachel, yuje, benwu

分享到: Facebook

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週四 8月 19, 2010 9:09 am

3.3.1 試著解釋volatile 修飾字為什麼出現那麼多種說法
(3.3.1是僅供參考的章節、可略過)

你可能會很疑惑,為什麼關於volatile變數有那種多種說法,這現象真怪。
以下我試著用自己觀察的結果來解釋為什麼會有這麼多種說法:

在說明為什麼短短的兩行規定會衍生出那麼多種解說方式前,
需要先了解現在的java語言,不只是一種在Oracle公司(註1.)所撰寫的JVM下所執行的程式碼,它已經變成跨平台的程式語言最基本應該提供的api標準了。
JVM只是一個抽象概念,而不是指特定團體所釋出的軟體。
任何人只要照著Java規格書開發出一個能照著ByteCode執行事務的軟體,不管在哪個作業系統上運作,都是一種JVM,
而不是只有昇陽公司所釋出的JRE套件才能算JVM。

因為Java是跨平台且不限定開發者的規格和標準,所以它的規格通常是用抽象概念來制定的,
而不會詳細規定製作JVM的細節,畢竟每個平台都不盡相同,這樣才不會限制JVM開發者在不同平台上開發的彈性。
例如前一小節提到的happens-before概念,我認為就是一種抽象的實作規格。

不管實作JVM者是用哪一種底層技術(語言),或是要怎麼運用底層技術都無所謂;
總之,一旦java程式設計者命令JVM執行某些會產生happens-before關係的程式碼,
那麼執行這些程式碼的虛擬機器就應該讓結果產生如同happens-before定義的效果。

至於為什麼坊間會有那麼多令人聽越迷糊的說法,我認為多半是因為他們用某個特定的JVM實作happen-before的方法來說明volatile的作用,
但這種說法就難以概括全部的JVM,因為不同JVM、不同作業系統,完成規格書制定效果之實作方法都不會完全相同。

就算在特定平台的某個JVM上,經由volatile修飾過的變數,具有3.3一開始某些文章所說的效果:
The value of volatile variable will never be cached thread-locally: all reads and writes will go straight to "main memory".

那也不代表在其他平台和其他JVM上,經由volatile修飾過的變數會產生一樣的效果啊!
如果有人寫出了一個JVM,能讓CPU暫存數值在快取裡面,
而且在這個JVM上面處理的變數經由volatile修飾過之後,其他執行緒還是可以讀取得到快取裡面值,這做法也不違反java規格書的內容!
但如果用剛才引述內容的解說方式,也就難怪大家看了一頭霧水,還是不曉得volatile在幹麻。

因此,我認為從寫java程式的角度來看,解釋多執行緒各種修飾字的使用效果才是重點
即使這種效果是抽象概念也沒關係,不同電腦的架構本來就不一樣,用JVM實作volatile修飾字的底層細節來說明它的效果,意義不大。

這種概念就像是學習Java語言者,不需要仔細探究JVM開發者如何使用底層非物件導向的語言,
實作出可以跑Java物件導向語言所撰寫的程式那樣。 過去所了解的類別、繼承、多型等等概念,何嘗不是抽象的?
但我們是透過這些概念來思考和寫程式,而不是用底層的語言來思考問題的。
不需要太過深入了解要怎麼用底層語言實作這些OOP的抽象觀念。
學Java多執行緒修飾字的過程也是這樣,太過於底層的細節不是我們主要的學習內容。

------附註-----
1. Oracle公司收購Sun了,現在Java的是Oracle的資產囉~
最後由 mtyjl 於 週五 8月 27, 2010 6:47 pm 編輯,總共編輯了 4 次。
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週五 8月 20, 2010 9:23 am

3.4 物件鎖定(locks)的概念

3.1討論到執行緒互相干擾時,提到一種解決方法,叫作「互斥」(mutually exclusive)。
互斥的作用是讓特定程式碼在同一時間點上,只交給一個執行緒執行。
這樣就算程式碼的執行是可以再分割的,也不怕分割之後會有其他執行緒存取特定區域的資料,干擾最後的運算結果。

在很多程式語言的發展過程中,引入了一種叫作lock的概念以實作互斥機制。
往後的文章中,統一將lock譯作「鎖定」或是「鎖」,這樣中文句子才會比較通順。但這兩種翻譯名稱指的是同一種東西。
如果等一下念起來還是怪怪的,請大家見諒。這已經是我找到最接近原文意思的翻譯名稱了。
「鎖定」這個詞聽起來像是動詞,感覺像是某種動作,但是在描述這種概念的原文文章中,lock幾乎都是名詞形態。

鎖定的概念是這樣的:
首先定義一下名詞,「需要讓執行緒彼此互斥的程式碼片段」會縮寫為「互斥的區域」。

為了製造出互斥的區域,以管理不同執行緒在特定時間點執行某些程式碼的權限,程式語言發展出一種抽像概念叫作lock( 鎖 )。
每個互斥區域都會有獨一無二的鎖,負責管理該區的執行權限以製造出互斥效果。
當執行緒要執行某個互斥區域的程式碼時,它需要先獲得這個區域的「鎖」;
這個鎖在同一時間只能讓一個執行緒擁有,持有「鎖」的執行緒才可以執行那個鎖管理的互斥區域。

由於每個互斥區域都只有一個鎖,擁有鎖定的執行緒才有互斥區域的執行權利,
因此一旦多個執行緒都要執行特定的互斥區域時,他們需要去搶這個互斥區域的鎖定。
接著會有某個執行緒獲勝,搶到鎖,開始執行互斥區域內的程式碼。
在此同時,搶失敗的執行緒就要等待互斥區域執行完畢後,那個獲勝的執行緒會釋出剛才獲得的鎖定,其他執行緒才有機會執行程式碼。

「鎖」可以防止多條執行緒同時存取某些資料,因此有些英文書中使用「守護」這個動詞來類比上一段的權限管理方式。
當名稱叫作MyLock的鎖,管理了某一變數num的操作指令之執行權限時,他們稱呼這種關係為「num由MyLock守護」。
換成原文的說法就是:
原文中 寫:num is guarded by MyLock.


以上就是「鎖」的概念。
也許我的口才很難讓大家立刻了解,因此講到synchronized修飾字時,會配合程式碼再解釋一次。

補上前幾段某些敘述的原文資料引用出處,供大家參考:
在 Java concurrency tutorial 的 Intrinsic Locks and Synchronization 寫:Every object has an intrinsic lock associated with it.
By convention, a thread that needs exclusive and consistent access to an object's fields has to acquire the object's intrinsic lock before accessing them,
and then release the intrinsic lock when it's done with them.
A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock.
As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock.


原文裡面提到了「intrinsic lock」,意思是隱含的鎖。這是什麼東西呢?
每個Java物件誕生之後,不管是否要讓多條執行緒同時操作,都會擁有獨一無二的鎖,
如此一來當程式撰寫者需要擺放一些互斥區塊在類別時,就能利用物件天生擁有的鎖來管理互斥區塊的執行權限。
這種鎖不像一般區域(field)的變數那麼公開,要是類別裡面沒有互斥區塊,也體會不到它的存在,
或許這就是為什麼會用「intrinsic」這個詞來形容這個鎖吧。

JVM裡面有個機制負責記錄究竟有多少執行緒想要獲得物件的隱含鎖。
官方說明文件提到:請大家想像一下,每個JVM裡的物件都有一個監視器(monitor),負責觀察並記錄所有想要獲得某個鎖的執行緒。
當執行緒想要擁有某個物件的隱含鎖(intrinsic lock),它就會去鎖定 (在此作動詞使用) 那個物件的監視器。
如果成功地鎖定了監視器,就等同於擁有了那個物件的鎖定。
或許是因為這層關係,所以Java官方的說明文件中,intrinsic lock和monitor lock這兩個詞經常交互使用,但指的是同一種東西。
有時monitor lock還會簡稱為monitor,這種用法常常讓母語是中文的我們看不懂到底原文想表達什麼。其實就只是鎖定的概念罷了。

最後附上原文說明,讓大家能體會剛才那段的說法:
出處和這小節的前一段原文位置是一樣的。
在 Java concurrency tutorial 的 Intrinsic Locks and Synchronization 寫:Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. (The API specification often refers to this entity simply as a "monitor.") Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object's state and establishing happens-before relationships that are essential to visibility.


3.4小節講的是「鎖定」概念,讓大家先了解程式碼同步化處理的理論背景。
下一小節開始談理論的實作方式之一,大名鼎鼎的synchronized修飾字。
最後由 mtyjl 於 週四 8月 26, 2010 1:24 pm 編輯,總共編輯了 3 次。
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週六 8月 21, 2010 7:01 am

3.5 synchronized 修飾字(modifier)

為了不要中英文混雜,讓大家看得很痛苦,以下「synchronized修飾過的程式碼區域」會簡稱為「同步化區域」;「使用synchronized修飾」會簡稱為「同步化處理」或是「同步化」。

synchronized可以為修飾的範圍製造互斥效果,前一小節「互斥區域」的概念就是這小節的「同步化區域」。
同步化區域實作了lock的抽象概念,當執行緒跑到同步化區域時,會嘗試獲得與這個同步化區域相關連的鎖。
獲得與同步化區域相關的鎖之後,其他執行緒就不能執行這個「鎖」守護(管理)的所有同步化區域,於是這些同步化處理過的程式碼片段就能讓執行緒互斥。

synchronized修飾的位置是一般方法(method)以及{}圍住的程式區塊,也就是有兩種方法製造同步化區域。
修飾方法時,要擺在方法回傳值的前面:
代碼: 選擇全部
public synchronized void someMethod(){
   //互斥內容
}

執行同步化的方法時,嘗試取得的鎖是含有這方法物件的隱含鎖(intrinsic lock)
如果能獲得隱含鎖,就可以執行這方法,否則就安份地等吧。

上一段的說明強調執行緒試著取得「物件的隱含鎖」是因為synchronized還能修飾一般程式區塊。
我們可以直接在類別的任意區域寫下:
代碼: 選擇全部
Synchronized ( 某個物件 ) {
//互斥內容
}

在這種同步化區域,執行緒嘗試取得的鎖是括號內「某個物件」的隱含鎖
而不一定是取得「擺放同步化區域的物件」之隱含鎖喔!

將「某個物件」改為this才會取得與上一段程式相同的鎖定。

synchronized除了能讓修飾區域產生執行緒互斥,它還可以使區域內變數的存取建立happens-before關係。
代碼: 選擇全部
public synchronized void someMethod(){
   System.out.println(num);
   num++;
}

以這同步化方法為例,前一個執行緒num++,離開同步化方法後,下一個進入someMethod()的執行緒一定能印出剛才執行緒num++更新後的數值,也就是同步化區域內沒有記憶體不一致問題

講了那麼多抽象概念,來看看程式碼經過synchronized修飾後的改變,
不需要非常仔細地讀程式碼,可以直接看底下的說明:
代碼: 選擇全部
public class SynchronizedTest {

    public static class Addition implements Runnable{
        public void run(){
           for( int i = 0 ; i < 1000 ; i ++ ){
              NoneSynchronizedBean.add(1);
           }//for
        }//run
    }//
   
    public static class Plus implements Runnable{
        public void run(){
           for( int i = 0 ; i < 1000 ; i ++ ){
              NoneSynchronizedBean.plus(1);
           }//for
        }//run
    }//
   
    public static class NoneSynchronizedBean{
        private static int num;
        public NoneSynchronizedBean() {
           num = 0;
        }
   
        public static /*synchronized*/ void add(int input){
           num += input;
        }
       
        public static void plus(int input){
           num += input;
        }
       
        public static /*synchronized*/ int get(){
           return num;
        }
    }//NoneSynchronizedBean
           
    public static void main(String[] args) {
        new Thread(new Addition() ).start();
        new Thread(new Addition() /*Plus()*/ ).start();
        try{ Thread.sleep(500); } catch(InterruptedException ie){ ie.printStackTrace(); }
        System.out.println( NoneSynchronizedBean.get() );
    }//main 
   
}//class

main執行緒產生的兩個Addition執行緒會各自加1000到NoneSynchronized類別。
但因為NoneSynchronized沒有經過同步化處理,所以最後main執行緒印出的數值往往不是2000。
取消add()和get()方法的敘述中包住synchronized的註解記號,再執行程式,輸出結果就正常了。

3.4.1的最後,提醒大家注意同步化區域的鎖定範圍!
物件鎖管理的是同步化區域的執行權限,而不是同步化區域裡面所有資料的修改權限


想像一下,假如你要設計的類別就像上面的程式碼那樣,有兩種方法add()和plus()可以改變num的值,結果卻不小心忘記用synchronized修飾plus()了。
此時,很不巧地,當你透過Addition執行緒操作add()方法改變num的過程中,有人使用了一個叫作Plus執行緒呼叫NoneSynchronizedBean的plus(),你覺得num的值會受影響嗎?

來做個實驗吧!
試著取消在以下這行程式碼裡面包住Plus()的註解,並且用註解包住Addition()。
代碼: 選擇全部
new Thread(new Addition() /*Plus()*/ ).start();

記得也要取消NoneSynchronizedBean類別裡面add()和get()包住synchronized的註解喔。
修改過的程式會有Addition和Plus執行緒分別使用同步化與非同步化方法改變num的值,我們來看看結果會怎樣吧?

再執行發現輸出的值超過1000,這表示隱含鎖的設計只能防止一個以上的執行緒進入相同「鎖」管轄的其他同步化區域,
而不能禁止其他執行緒改變num的值。

這現象意謂著什麼呢?
當初想製造互斥區域的原因,不就是要讓執行緒跑那個區域的程式碼有先後順序嗎?
這樣程式才會照著原始的設計規格,得到預期的數值。
如果設計物件過程中,不小心讓別的執行緒從一般的方法改變同步化區域裡面存取的變數,這種程式的執行結果或許就違反設計的規格囉。
最後由 mtyjl 於 週三 8月 25, 2010 10:28 am 編輯,總共編輯了 3 次。
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週日 8月 22, 2010 7:50 am

3.6 何謂Thread-safe?

volatile和synchronized修飾字是設計物件的基礎工具,可以讓多執行緒同時操作一個物件而不會產生超出預期的狀況。
但是從前一小節的例子可以看出,如果修飾字的用法和使用位置不正確,一樣可能產生意料之外的結果。
由此可以想見,「如何設計一個能讓多執行緒操作而不產生問題的物件」是一個值得好好討論的題目。

在討論過程中,大家常常用一種口語化的形容詞代表「讓多執行緒同時操作而不產生問題的物件」叫作Thread-safe
可是單就何謂「讓多執行緒操作而不產生問題的物件」? 就沒有很明確的定義。
這也使得每個人對Thread-safe的認知都太不一樣,例如以下說法:
常見的說法 寫:..can be called from multiple program threads without unwanted interactions between the threads.
..may be called by more than one thread at a time without requiring any other action on the caller’s part

看完還真是令人完全摸不著頭緒啊!

為了不要讓後面介紹的內容提到「Thread-safe」時,每個人腦海浮現的概念都不一樣,
所以我決定引述Brian Goetz 在Java Concurrency In Practice書中的解釋,對「Thread safe」下一個比較嚴謹的定義,
讓大家能體會什麼是thread-safe:

既然我們是依照程式的執行結果判斷「什麼樣的物件讓多執行緒操作而不產生問題」,因此可以了解這些說法的核心其實是在探討「正確」問題。
程式的執行結果與我們制定的物件設計規格相符,我們才會說這個物件是「正確的」、「沒有問題的」、「符合預期的」。

有了這一層認識,現在可以替Thread-safe下定義了:
如果程式撰寫者命令多個執行緒同時執行某個物件封裝的程式碼時,可以不用顧慮任何作業系統的排程方式,
也不需要額外的同步化處理或是協調執行緒的機制,就能得到正確的執行結果,那麼這個物件就是「執行緒安全」的。


Thread-safe定義的原文內容, 在Java Concurrency In Practice 寫:A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.

中文和英文的語法與思考方式有很多地方不同,因此我用中文重新表達原文的意思,而不逐字逐句翻譯。

定義名詞的過程總是很無聊的,但是在那之後,就可以開始討論怎麼樣設計「執行緒安全」的類別,也可以開始介紹Java內建的多執行緒工具囉!
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週二 8月 24, 2010 9:43 am

3.7 利用Java提供的套件:Atomic

雖然synchronized是個能夠同時解決很多問題的修飾字,但是這有代價的,就是執行效率會降低。

做個實驗給你看:
(不需要仔細看懂程式碼,底下會直接說明。貼出程式碼只是讓想要玩的人可以拿去跑跑看)
代碼: 選擇全部
public class SynchronizedCounterDemo {
   
    public static class SynchronizedCounter {
        private static int num = 0;
       
        public static synchronized int addNum(int add){ return ++num; }
        public static synchronized int decNum(int dec){ return --num; }
                          
    }//synchronizedObject
   
    public static class OnlineOperation implements Runnable{
       public void run(){
          long beginTime = System.currentTimeMillis();
          for( int i = 0 ; i < 1000000 ; i ++ ){
             SynchronizedCounter.addNum(1);
          }
          long endTime = System.currentTimeMillis();
          System.out.println( ( endTime - beginTime ) + " milliseconds. ");
       }//run
    }//Operation
           
    public static void main(String[] args) {
        new Thread(new OnlineOperation() ).start();
        new Thread(new OnlineOperation() ).start();
    }//main
   
}

請試著想像一下,你要在某個servlet上面提供一個計數器,負責記載某一個網頁面被人推薦與反推薦的次數,並將結果顯示在網頁上。
於是你寫了一個SynchronizedCounter封裝了num變數,提供addNum()和decNum()這兩個方法改變這個評分的值。
在程式的main方法裡面產生的兩個OnlineOperation執行緒則是模擬了不同使用者呼叫addNum() 的過程。

呼叫addNum()的執行緒會先讀取原本的值,然後加1到上面,再寫回紀錄,最後回傳數值給呼叫方法的執行緒。
為了不讓執行緒呼叫這種可以分割成多個步驟的方法時,出現錯誤的運算結果,我們得用synchronized修飾SynchronizedCounter對外互動的方法。

問題是這樣做會降低執行效率,因為一個物件裡的同步化方法是由相同的鎖守護的。
就算不同執行緒操作的變數彼此都不影響,那個沒有拿到鎖定的執行緒還是只能乖乖等待。

你可以跑一次程式,得到synchronized修飾下兩個OnlineOperation的執行消耗時間,然後用註解包住synchronized,再跑一次看看。
以下是我電腦的輸出結果:
有synchronized修飾字的輸出結果 寫:797 milliseconds.
797 milliseconds.

無synchronized修飾字的輸出結果 寫:32 milliseconds.
32 milliseconds.

天哪!花費時間差了約25倍啊! synchronized犧牲的效率可真多!

如果有一種解決方案,讓大家不需要同步化變數存取的過程,而且又不會犧牲很多效率,該有多好?
這就是Atomic套件的用途!

Atomic套件封裝了一系列的原生型態,並透過這些類別的方法封裝變數的存取過程與一些常見的判斷邏輯,使這些存取操作過程都變成不可分割的運算,
從此之後你不用再擔心變數的存取過程會出錯,這些類別完全是執行緒安全的!
可以安心地讓許多執行緒呼叫這些套件提供的方法,修改變數的值。

java.concurrent.atomic套件方法的命名有統一的原則:
出現get,意思就是會讀取變數的值,但過程中不會產生記憶體不一致問題,全部操作過程也不會分割。
出現set,意思就是會設定變數的值,過程中一樣不產生記憶體不一致問題,全部操作過程一樣不分割。

AtomicInteger為例,它封裝了整數,提供多種與整數互動的方法:
addAndGet(value),可以加value到原本封裝的值上,再回傳運算結果;getAndAdd(value)也會改變原本的值,
但是就如同字面上的意思,這方法會先回傳原本的值才再加value上去。

很多atomic套件的方法封裝了常出現的變數處理邏輯,呼叫這些方法可以少寫很多if-else程式區塊。
例如compareAndGet(expect, update)會比較原本的值是不是和expect參數相同,如果相同就設定這個值為update並回傳true,否則就回傳false。
這種功能相當於縮短以下程式碼(模擬的程式)長度:
代碼: 選擇全部
public boolean compareAndGet(expectedValue, updateValue) {
if ( AtomicObject.get() == expectedValue ) { 
AtomicObject.set( updateValue ); 
    return true;
}
else {  return false; }
}

怎麼樣?是不是很省力呢?

那麼又為什麼麼會有個方法叫作weakCompareAndSet(expect, update)?
這方法做的事情和compareAndSet()相同,只是處理細節的方式有些差異。

weakCompareAndSet()讀取變數的過程和compareAndSet()相同,都是不可分割的運算,
差別在寫回資料時,weakCompareAndSet()的操作過程沒有解決記憶體不一致問題,也就是某些情況下會讀取不到最後更新的數值。
因此,weakCompareAndSet()回傳的布林值僅供參考,不一定合乎理想的執行邏輯....
話說回來,這種不嚴謹的資料處理過程也有優點,就是執行效率比較好。
至於資料的嚴謹程度和執行效率之間要怎麼取搶,就看程式設計者的決定了。

此外atomic套件還有一系列的方法,但是從命名方式就能讓大家一目了然有什麼功能,我就不多提了。

有興趣知道atomic套件的底層是怎麼實作的,可以參考:
著名的多執行緒專家 Brian Goetz 在IBM DeveloperWorks寫的Java Theory And Practice: Going Atomic

想瞭解更多atomic套件的內容,可以參考:
官方atomic 套件 api
非官方翻譯之繁體中文atomic套件 api

這小節的最後,我們用AtomicInteger類別重新製作SynchronizedObjectOperation,看看運算的效率如何:
代碼: 選擇全部
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
       
    public static class SynchronizedObject {
        private static AtomicInteger num1 = new AtomicInteger(0);
        private static AtomicInteger num2 = new AtomicInteger(0);
       
        public static void addNum1(int i){ num1.addAndGet(i); }
       
        public static void addNum2(int j){ num1.addAndGet(j); }       
    }//synchronizedObject
   
    public static class Operation1 implements Runnable{
       public void run(){
          long beginTime = System.currentTimeMillis();
          for( int i = 0 ; i < 1000000 ; i ++ ){
             SynchronizedObject.addNum1(i);
          }
          long endTime = System.currentTimeMillis();
          System.out.println( ( endTime - beginTime ) + " milliseconds. ");
       }//run
    }//Operation1
       
    public static class Operation2 implements Runnable{
       public void run(){
          long beginTime = System.currentTimeMillis();
          for( int i = 0 ; i < 1000000 ; i ++ ){
             SynchronizedObject.addNum2(i);
          }
          long endTime = System.currentTimeMillis();
          System.out.println( ( endTime - beginTime ) + " milliseconds. ");
       }//run       
    }//operation2
           
    public static void main(String[] args) {
        new Thread(new Operation1() ).start();
        new Thread(new Operation2() ).start();
    }//main
   
}


在我電腦測試的結果如下:
使用AtomicInteger改寫SynchronizedCounter的執行消耗時間 寫:250 milliseconds.
265 milliseconds.

這結果比起使用synchronized修飾字的計數器,執行消耗時間足足少了0.5秒多,提升很多執行效率。
最後由 mtyjl 於 週三 8月 25, 2010 9:38 am 編輯,總共編輯了 1 次。
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章ghost3401 » 週三 8月 25, 2010 1:23 am

恐龍書那邊

我覺得應該恐龍書應該只是提到處理PROCESS的一些大原則
不算避重就輕~~
-------------------------------
繼續加油~~~文章很精彩(雖然我不熟thread的程式設計~~
ghost3401
繼續深造的研究生
 
文章: 473
註冊時間: 週四 9月 07, 2006 9:07 am
來自: 基隆偏遠地帶

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週四 8月 26, 2010 1:11 pm

3.8 設定共享物件為不變的物件(immutable object)以減少錯誤發生機會

3.1小節曾經用程式碼示範什麼是執行緒互相干擾,
我們製造NoneSynchronizedClass類別封裝兩個變數,接著透過ThreadInterference類別產生兩個執行緒不斷地設定NoneSynchronizedClass的欄位(field),
最後呼叫checkNameAndIDEqual()比比看兩個變數第一個字元是否和原先設定的一樣。

現在我們都曉得這樣會出錯,因為比較字元的方法checkNameAndIDEqual()需要讀取隨時會變的欄位,而且兩個變數個別的讀取之間可以插入其他指令。
為了解決這問題,或許我們得用synchronized修飾相關的方法。

但是,我們在乎的其實只是兩個變數第一個字元是否相同,而不是比較變數的第一個字元是否和某個特定的值相同,
真有必要使用synchronized這種功能那麼多那麼強,又會減緩執行效率的解決方式嗎?
會不會殺雞用牛刀了?

如果可能的話…就讓兩個變數設定後不能再修改,那checkNameAndIDEqual()應該就不會跑出錯誤訊息了吧?

順著思緒,回想一下,Java語言有什麼設計可以防止修改變數?..............這不就是final修飾字的用途嗎?

final除了可以在類別的欄位預先設定常數,還可以等到建構物件時才設定
以這個封裝資訊的JavaBean為例,final修飾的欄位在建構式指定是沒問題的:

代碼: 選擇全部
public class ImmutableJavaBean{
private final String name;
    private final int num;
public ImmutableJavaBean(String name, int num){
        this.name = name;
        this.num = num;
}
public String getName(){ return name; }
    public int getNum() { return num; }
}

像這種建構之後就不能修改的物件,有個慣用名稱叫作Immutable Object,以後將統一翻譯為「不變的物件」

程式開始執行後,產生不變的物件可以動態設定要封裝的資訊,
且不用擔心其他執行緒偷改數值,是理想的多執行緒資訊交流工具

看到這裡你想必會問,如果NoneSynchronizedClass的欄位都用final修飾,那變數在此之後就不能修改,兩個執行緒不就只能交流資訊一次?

其實,我們可以不用死腦筋,非得透過共享存放資訊的物件欄位才能交換訊息。
執行緒可以共享物件的參考,而不用同時共享許多變數,製造大量資訊同步問題!

如果3.1的例子不要透過方法設定NoneSynchronizedClass的欄位,改成傳一個的封裝資訊的物件(簡稱為JavaBean)參考給NoneSynchronizedClass儲存,那麼比對欄位第一個字元的結果不就正常了嗎?
之後當其他執行緒要改變某些狀態時,再傳一個新產生的不可變物件之參考給NoneSynchronizedClass保存,這樣就能交換訊息囉~

執行緒透過JavaBean交流,好處是變數的存取點只有一個
程式設計者只需要考慮單一存取點的同步化,而不需一次考慮多個變數值的存取過程中有沒有其他執行緒干擾結果。

在3.3講到不可分割運算時 寫:Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double)


由這樣的設計可以知道,參考的寫入和讀取分別都不可分割,使我們能夠更新NoneSynchronizedClass存放的資訊,
同時又防止其他執行緒修改原先設定好的欄位。

因為就算A執行緒呼叫checkNameAndIDEqual()之前,B傳了一個新的JavaBean實例給NoneSynchronizedClass參考,
這JavaBean的欄位也不可能有變化而使得checkNameAndIDEqual()回傳false!

現在讓我們利用不可變的物件修改3.1的例子。
NoneSynchronizedClass現在改為ImmutableObjectExample:

代碼: 選擇全部
public class ImmutableObjectExample {
    private final String name;
    private final String id;

    public ImmutableObjectExample(String name, String id) {
       this.name = name;
        this.id = id;
    }   
    public boolean checkNameAndIDEqual() {
       return ( name.charAt(0) == id.charAt(0)) ? true : false;
    }//checkNameAndIDEqual     
}//class


至於ThreadInterference則變為ImmutableObjectCreator。

代碼: 選擇全部
public class ImmutableObjectCreator {
    private static /*volatile*/ ImmutableObjectExample IOE;
           
    public static void main(String[] args) {
       IOE = new ImmutableObjectExample("SomeName", "SomeID");
        Thread one = new Thread(new AccessThread("John", "J") );       
        Thread two = new Thread(new AccessThread("Peter", "P") );     
        one.start();
        two.start();
    }//main
   
    public static class AccessThread implements Runnable{
        private String name;
        private String id;
        public AccessThread(String name, String id){
           this.name = name;
           this.id = id;
        }
        public void run(){
           for ( int i = 0 ; i < 10000 ; i ++ ){
              IOE = new ImmutableObjectExample(name, id);
              if ( !IOE.checkNameAndIDEqual() ) {
                  System.out.println(i + ") illegal name or ID.....");
              }//if
           }//for
           System.out.println("Process finished.");                        
        }//run       
}//AccessThread
}//class

ImmutableObjectCreator產生的多執行緒只改變共享物件的參考
因此整個程式所有的方法都不需要同步化,一樣能讓執行緒不互相干擾!


甚至,如果執行緒只是單純比對物件內部的值,而不在乎同一個變數前後儲存的物件彼此之間有什麼關係,
那麼保存參考的變數 ( 在這個例子中是IOE ) 甚至可以不用volatile修飾!
畢竟就算讀取到舊的物件,也不會因為比對數值不同而出現錯誤訊息。

最後,你可能還是會有點疑惑,3.2不是說過「執行緒不會理所當然地能夠讀取到其他執行緒寫入變數的值」嗎?
那麼要初始化不可變的物件時,為什麼執行緒就可以正確地讀取到別的執行緒所建構的物件欄位呢?
建構式難道不用同步化嗎? 在建構式裡面存取的變數不需要用volatile修飾嗎?
畢竟有些變數宣告之後就有預設值,即便建構式沒有設定,執行過程也不一定會丟出例外呀~~
這是不是很令人納悶呢?

這邊要注意,
建構式不能用synchronized修飾! 這是語言的規定!
但你不需要擔心同步問題,原因是建構式裡面的變數存取與建構式外的變數存取之間會建立happens-before關係,出處在這裡
在Java Memory Model的Threads and Locks小節 寫:The default initialization of any object happens-before any other actions (other than default-writes) of a program.

此外final修飾過的變數必須在欄位或是建構式給定初始值,否則會無法通過編譯。
這也就是為什麼要建議採用final修飾欄位的JavaBean傳遞訊息,如此一來可以避免不正常地物件欄位初始化過程。

由這些語言規格可以了解,執行緒一定能讀取到欄位經過final修飾的類別所產生之實例(instance)。
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週一 8月 30, 2010 4:44 pm

第三章快完成了,不曉得大家覺得寫到這邊會不會太複雜,太冗長難懂呢?
其實我也為這件事大傷腦筋....

為了不要信口開河隨便亂說,引用沒有根據的說法,所以查資料的過程都非常小心。
可是,在前一小節之後的內容遠比當初估計的還要複雜。
要怎麼有根據有系統地解釋就變得很麻煩,而且開學後碰到的領域也和多執行緒比較沒關係。
因此開學前我打算先完成一些事,有空再慢慢補齊多執行緒這邊的內容。

之後還是會來這邊逛,只是也許會分享其他和我們比較有關的資料,而不一定很快完成多執行緒的學習心得。
如果有問題或是有錯也歡迎您提出來,謝謝~
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章mtyjl » 週一 9月 06, 2010 10:44 pm


剛才去天瓏, 意外發現恐龍書第八版居然出了with java的版本!!!
還熱騰騰地剛進口咧~

裡面不止教了基本的Thread api, 還提到一些更進階的工具, 像是Thread pool executor之類的
而且有關java的內容都是引用規格書的, 而不是隨便參考資料就寫上去

暈倒....那這跟我原本想寫的東西不就差不了多少嗎? ....他書中提到的OS基礎觀念還深很多咧....

既然系上作業系統的課本幾乎都用恐龍書
那我想....是該修改一下這系列的內容了,一些不太正確的引用資料也會一起更正
以後的文章應該盡量跟恐龍書的內容區隔開來....
有空的話我會解析一些大型開放源碼的軟體, 看看裡面是怎麼設計的
mtyjl
漸有心得的高中生
 
文章: 136
註冊時間: 週六 9月 13, 2008 2:05 pm

Re:[分享]Java多執行緒程式設計

文章b80203 » 週二 9月 07, 2010 9:07 pm

mtyjl 寫:
剛才去天瓏, 意外發現恐龍書第八版居然出了with java的版本!!!
還熱騰騰地剛進口咧~

裡面不止教了基本的Thread api, 還提到一些更進階的工具, 像是Thread pool executor之類的
而且有關java的內容都是引用規格書的, 而不是隨便參考資料就寫上去

暈倒....那這跟我原本想寫的東西不就差不了多少嗎? ....他書中提到的OS基礎觀念還深很多咧....

既然系上作業系統的課本幾乎都用恐龍書
那我想....是該修改一下這系列的內容了,一些不太正確的引用資料也會一起更正
以後的文章應該盡量跟恐龍書的內容區隔開來....
有空的話我會解析一些大型開放源碼的軟體, 看看裡面是怎麼設計的


兩年多前我修os的時候,就已經有java版本了~~
雖說那時候是末代的C++,不過班上買java的人不少呀~~

讚賞你寫這些教學文章的熱情,因為要用自己的話寫出內容
相對一定要了解這些領域,加油囉!!你可以試試多以圖片的方式表現@@

雖然滿想看你寫的這些文章,因為我也不是很熟執行緒,只會點皮毛~~
沒很多時間看這麼多字,倒是看了一些程式碼~~如果多一些概念圖或流程圖的話~~
就會很棒了,畢竟一張圖勝過千言萬語呀~~

這暑假我也學了一些工夫,內容有組語、程式的編譯、連結與執行細節、Objective-C與Foundation框架
有多點的空閒時間,再來版上分享~~
b80203
資管系課程助教
 
文章: 189
註冊時間: 週日 10月 01, 2006 7:33 pm
來自: fju im & pe

Re: [分享]Java多執行緒程式設計

文章babybabygg1 » 週五 4月 07, 2017 1:23 pm

mtyjl 寫:在這一系列文章中,我想分享的是有關 Java 多執行緒 的學習心得。

為什麼會突然丟出這個話題呢?
因為我在學 Java 多執行緒設計時,吃足了苦頭…… Orz。
多執行緒牽涉的層次很廣,關係到 CPU 和 OS 的運作概念、Java 實作理論的方式,
而且它常常含概在其他領域中一起出現,例如講 Socket programming 的書,一定會提到多執行緒,
畢竟一般 I/O 的 Java 網路程式,等待 client 端連入時,處理 I/O 的執行緒是在 blocked 狀態。

這現象對過去沒寫過多執行緒的人來說,最傷腦筋的就是資料分散又沒有系統,看完總是一知半解。
有些書只講理論,卻難以想像實際運作時的樣子;
只看到程式碼,卻又完全不了解背後運作的原理,以及選擇這樣設計的原因。
總之,很多文章都會提到多執行緒,但因為這東西本身就是複雜又麻煩的,
因此撰文者往往假設讀者對某部分資訊已經非常了解,以致於沒有一系列文章真的把理論和實作講得清楚又完整

例如:
Java 入門書 如果提到多執行緒,往往只講皮毛,卻不介紹進階的工具。偏偏基礎工具寫出來的程式太簡單,跟玩具沒兩樣。
OS 書 會講理論,但實際運作的樣子就要大家自行想像。
Java 網路程式的書 有多執行緒的實例,卻沒解釋清楚執行緒之間資訊交換的設計模式以及如何達到 thread safe…...
昇陽公司的教學文件兼具理論與實作,但是有些地方講的還是太少了,他們想等你去買昇陽公司的多執行緒專書。

以上這些事,是我在讀完良葛格的書、參考過昇陽公司的官方文件,
還有一堆網路文章和坊間的範例,稍有一點心得時,忽然意識到的事實。
然後我就想,若是這樣,何不學學良葛格和鳥哥那種高手,統整各類資訊,整理一些筆記方便大家入門,順便複習呢?

於是這系列文章就誕生了……
在以下的文章中,會從 Java 實務的角度,描述多執行緒可能遇到的問題,
如果能找到的話,還會提供各種正確與錯誤的例子給大家參考。
接著,再來看看昇陽公司究竟為程式設計者準備了哪些工具,而這些工具又是怎麼運用的,
並探討多執行緒程式設計原則。

之後我會循序漸進,每幾天丟出一篇文章和大家討論,希望能拋磚引玉,吸引更多人分享精彩的內容。
如果內容有錯,或是有不夠清楚透徹的地方,也請大家反應讓我了解,
經過這裡的路人如果有發現錯誤,也希望您可以來信指正,謝謝大家

我的信箱是 youjenli1124@yahoo.com.tw


以下是將要介紹 Java 多執行緒程式設計 的大致內容順序:
含有EX的地方是有例子補充說明的小節

1. Thread(執行緒)


2. 執行緒的開始


3. 真相,只有一個嗎?執行緒共享資料產生的問題


4. 我們真的很沒默契,執行緒卡住了:Liveness
    4.1 可怕又無直接解決方法的DeadLock EX: DeadLock實例
    4.2 Starvation
    4.3 LiveLock


5. 執行緒開始之後的管理
    5.1 縱向的觀點:Thread類別所提供的方法
    5.1 為什麼Java不建議使用suspend(), stop()
    5.2 橫向的觀點:由Object繼承而來的方法 notify(), yield(), wait() EX: nofity()與wait()的實例
      5.2.1 suspend()不行,為什麼wait()就可以?
    5.3 靈活運用TheadLocal類別


以上大致就是我想介紹的流程。之後一定還會再變動
除了第六部份,因為是高階同步工具,比較難找到簡短易懂的實例之外,其他地方我都盡可能找出實例,或著自己來撰寫爛爛的例子


所有例子如果參考,或是靈感來自別人的文章,版權都是別人的,我也會隨即附上出處。
要是哪裡不小心忘記了,麻煩大家通知我趕快補上。


我常常都想深入了解java, 但好像很難明.....
babybabygg1
剛學走路的小朋友
 
文章: 5
註冊時間: 週五 4月 07, 2017 11:12 am

上一頁

回到 java討論區

誰在線上

正在瀏覽這個版面的使用者:沒有註冊會員 和 5 位訪客

cron