Java并發(fā)性與多線程介紹
在過去單CPU時(shí)代,單任務(wù)在一個(gè)時(shí)間點(diǎn)只能執(zhí)行單一程序。之后發(fā)展到多任務(wù)階段,計(jì)算機(jī)能在同一時(shí)間點(diǎn)并行執(zhí)行多任務(wù)或多進(jìn)程。雖然并不是真正意義上的“同一時(shí)間點(diǎn)”,而是多個(gè)任務(wù)或進(jìn)程共享一個(gè)CPU,并交由操作系統(tǒng)來完成多任務(wù)間對(duì)CPU的運(yùn)行切換,以使得每個(gè)任務(wù)都有機(jī)會(huì)獲得一定的時(shí)間片運(yùn)行。
隨著多任務(wù)對(duì)軟件開發(fā)者帶來的新挑戰(zhàn),程序不在能假設(shè)獨(dú)占所有的CPU時(shí)間、所有的內(nèi)存和其他計(jì)算機(jī)資源。一個(gè)好的程序榜樣是在其不再使用這些資源時(shí)對(duì)其進(jìn)行釋放,以使得其他程序能有機(jī)會(huì)使用這些資源。
再后來發(fā)展到多線程技術(shù),使得在一個(gè)程序內(nèi)部能擁有多個(gè)線程并行執(zhí)行。一個(gè)線程的執(zhí)行可以被認(rèn)為是一個(gè)CPU在執(zhí)行該程序。當(dāng)一個(gè)程序運(yùn)行在多線程下,就好像有多個(gè)CPU在同時(shí)執(zhí)行該程序。
多線程比多任務(wù)更加有挑戰(zhàn)。多線程是在同一個(gè)程序內(nèi)部并行執(zhí)行,因此會(huì)對(duì)相同的內(nèi)存空間進(jìn)行并發(fā)讀寫操作。這可能是在單線程程序中從來不會(huì)遇到的問題。其中的一些錯(cuò)誤也未必會(huì)在單CPU機(jī)器上出現(xiàn),因?yàn)閮蓚€(gè)線程從來不會(huì)得到真正的并行執(zhí)行。然而,更現(xiàn)代的計(jì)算機(jī)伴隨著多核CPU的出現(xiàn),也就意味著不同的線程能被不同的CPU核得到真正意義的并行執(zhí)行。
如果一個(gè)線程在讀一個(gè)內(nèi)存時(shí),另一個(gè)線程正向該內(nèi)存進(jìn)行寫操作,那進(jìn)行讀操作的那個(gè)線程將獲得什么結(jié)果呢?是寫操作之前舊的值?還是寫操作成功之后
的新值?或是一半新一半舊的值?或者,如果是兩個(gè)線程同時(shí)寫同一個(gè)內(nèi)存,在操作完成后將會(huì)是什么結(jié)果呢?是第一個(gè)線程寫入的值?還是第二個(gè)線程寫入的值?
還是兩個(gè)線程寫入的一個(gè)混合值?因此如沒有合適的預(yù)防措施,任何結(jié)果都是可能的。而且這種行為的發(fā)生甚至不能預(yù)測(cè),所以結(jié)果也是不確定性的。
Java的多線程和并發(fā)性
Java是最先支持多線程的開發(fā)的語言之一,Java從一開始就支持了多線程能力,因此Java開發(fā)者能常遇到上面描述的問題場(chǎng)景。這也是我想為Java并發(fā)技術(shù)而寫這篇系列的原因。作為對(duì)自己的筆記,和對(duì)其他Java開發(fā)的追隨者都可獲益的。
該系列主要關(guān)注Java多線程,但有些在多線程中出現(xiàn)的問題會(huì)和多任務(wù)以及分布式系統(tǒng)中出現(xiàn)的存在類似,因此該系列會(huì)將多任務(wù)和分布式系統(tǒng)方面作為參考,所以叫法上稱為“并發(fā)性”,而不是“多線程”。
多線程的優(yōu)點(diǎn)
盡管面臨很多挑戰(zhàn),多線程有一些優(yōu)點(diǎn)使得它一直被使用。這些優(yōu)點(diǎn)是:
- 資源利用率更好
- 程序設(shè)計(jì)在某些情況下更簡(jiǎn)單
- 程序響應(yīng)更快
資源利用率更好
想象一下,一個(gè)應(yīng)用程序需要從本地文件系統(tǒng)中讀取和處理文件的情景。比方說,從磁盤讀取一個(gè)文件需要5秒,處理一個(gè)文件需要2秒。處理兩個(gè)文件則需要:
- 5 seconds reading file A
- 2 seconds processing file A
- 5 seconds reading file B
- 2 seconds processing file B
- -----------------------
- 14 seconds total
從磁盤中讀取文件的時(shí)候,大部分的CPU時(shí)間用于等待磁盤去讀取數(shù)據(jù)。在這段時(shí)間里,CPU非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用CPU資源??聪旅娴捻樞颍?/p>
- 5 seconds reading file A
- 5 seconds reading file B + 2 seconds processing file A
- 2 seconds processing file B
- ----------------------
- 12 seconds total
CPU等待第一個(gè)文件被讀取完。然后開始讀取第二個(gè)文件。當(dāng)?shù)诙募诒蛔x取的時(shí)候,CPU會(huì)去處理第一個(gè)文件。記住,在等待磁盤讀取文件的時(shí)候,CPU大部分時(shí)間是空閑的。
總的說來,CPU能夠在等待IO的時(shí)候做一些其他的事情。這個(gè)不一定就是磁盤IO。它也可以是網(wǎng)絡(luò)的IO,或者用戶輸入。通常情況下,網(wǎng)絡(luò)和磁盤的IO比CPU和內(nèi)存的IO慢的多。
程序設(shè)計(jì)更簡(jiǎn)單
在單線程應(yīng)用程序中,如果你想編寫程序手動(dòng)處理上面所提到的讀取和處理的順序,你必須記錄每個(gè)文件讀取和處理的狀態(tài)。相反,你可以啟動(dòng)兩個(gè)線程,每
個(gè)線程處理一個(gè)文件的讀取和操作。線程會(huì)在等待磁盤讀取文件的過程中被阻塞。在等待的時(shí)候,其他的線程能夠使用CPU去處理已經(jīng)讀取完的文件。其結(jié)果就
是,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中。這會(huì)帶來磁盤和CPU利用率的提升。而且每個(gè)線程只需要記錄一個(gè)文件,因此這種方式也很容易編程實(shí)現(xiàn)。
程序響應(yīng)更快
將一個(gè)單線程應(yīng)用程序變成多線程應(yīng)用程序的另一個(gè)常見的目的是實(shí)現(xiàn)一個(gè)響應(yīng)更快的應(yīng)用程序。設(shè)想一個(gè)服務(wù)器應(yīng)用,它在某一個(gè)端口監(jiān)聽進(jìn)來的請(qǐng)求。當(dāng)一個(gè)請(qǐng)求到來時(shí),它去處理這個(gè)請(qǐng)求,然后再返回去監(jiān)聽。
服務(wù)器的流程如下所述:
- while(server is active){
- listen for request
- process request
- }
如果一個(gè)請(qǐng)求需要占用大量的時(shí)間來處理,在這段時(shí)間內(nèi)新的客戶端就無法發(fā)送請(qǐng)求給服務(wù)端。只有服務(wù)器在監(jiān)聽的時(shí)候,請(qǐng)求才能被接收。另一種設(shè)計(jì)是,監(jiān)聽線程把請(qǐng)求傳遞給工作者線程(worker thread),然后立刻返回去監(jiān)聽。而工作者線程則能夠處理這個(gè)請(qǐng)求并發(fā)送一個(gè)回復(fù)給客戶端。這種設(shè)計(jì)如下所述:
- while(server is active){
- listen for request
- hand request to worker thread
- }
這種方式,服務(wù)端線程迅速地返回去監(jiān)聽。因此,更多的客戶端能夠發(fā)送請(qǐng)求給服務(wù)端。這個(gè)服務(wù)也變得響應(yīng)更快。
桌面應(yīng)用也是同樣如此。如果你點(diǎn)擊一個(gè)按鈕開始運(yùn)行一個(gè)耗時(shí)的任務(wù),這個(gè)線程既要執(zhí)行任務(wù)又要更新窗口和按鈕,那么在任務(wù)執(zhí)行的過程中,這個(gè)應(yīng)用程
序看起來好像沒有反應(yīng)一樣。相反,任務(wù)可以傳遞給工作者線程(word
thread)。當(dāng)工作者線程在繁忙地處理任務(wù)的時(shí)候,窗口線程可以自由地響應(yīng)其他用戶的請(qǐng)求。當(dāng)工作者線程完成任務(wù)的時(shí)候,它發(fā)送信號(hào)給窗口線程。窗口
線程便可以更新應(yīng)用程序窗口,并顯示任務(wù)的結(jié)果。對(duì)用戶而言,這種具有工作者線程設(shè)計(jì)的程序顯得響應(yīng)速度更快。
多線程的代價(jià)
從一個(gè)單線程的應(yīng)用到一個(gè)多線程的應(yīng)用并不僅僅帶來好處,它也會(huì)有一些代價(jià)。不要僅僅為了使用多線程而使用多線程。而應(yīng)該明確在使用多線程時(shí)能多來的好處比所付出的代價(jià)大的時(shí)候,才使用多線程。如果存在疑問,應(yīng)該嘗試測(cè)量一下應(yīng)用程序的性能和響應(yīng)能力,而不只是猜測(cè)。
設(shè)計(jì)更復(fù)雜
雖然有一些多線程應(yīng)用程序比單線程的應(yīng)用程序要簡(jiǎn)單,但其他的一般都更復(fù)雜。在多線程訪問共享數(shù)據(jù)的時(shí)候,這部分代碼需要特別的注意。線程之間的交互往往非常復(fù)雜。不正確的線程同步產(chǎn)生的錯(cuò)誤非常難以被發(fā)現(xiàn),并且重現(xiàn)以修復(fù)。
上下文切換的開銷
當(dāng)CPU從執(zhí)行一個(gè)線程切換到執(zhí)行另外一個(gè)線程的時(shí)候,它需要先存儲(chǔ)當(dāng)前線程的本地的數(shù)據(jù),程序指針等,然后載入另一個(gè)線程的本地?cái)?shù)據(jù),程序指針
等,最后才開始執(zhí)行。這種切換稱為“上下文切換”(“context
switch”)。CPU會(huì)在一個(gè)上下文中執(zhí)行一個(gè)線程,然后切換到另外一個(gè)上下文中執(zhí)行另外一個(gè)線程。
上下文切換并不廉價(jià)。如果沒有必要,應(yīng)該減少上下文切換的發(fā)生。
你可以通過維基百科閱讀更多的關(guān)于上下文切換相關(guān)的內(nèi)容:
http://en./wiki/Context_switch
增加資源消耗
線程在運(yùn)行的時(shí)候需要從計(jì)算機(jī)里面得到一些資源。除了CPU,線程還需要一些內(nèi)存來維持它本地的堆棧。它也需要占用操作系統(tǒng)中一些資源來管理線程。我們可以嘗試編寫一個(gè)程序,讓它創(chuàng)建100個(gè)線程,這些線程什么事情都不做,只是在等待,然后看看這個(gè)程序在運(yùn)行的時(shí)候占用了多少內(nèi)存。
如何創(chuàng)建并運(yùn)行java線程
Java線程類也是一個(gè)object類,它的實(shí)例都繼承自java.lang.Thread或其子類。 可以用如下方式用java中創(chuàng)建一個(gè)線程:
- Tread thread = new Thread();
執(zhí)行該線程可以調(diào)用該線程的start()方法:
在上面的例子中,我們并沒有為線程編寫運(yùn)行代碼,因此調(diào)用該方法后線程就終止了。
編寫線程運(yùn)行時(shí)執(zhí)行的代碼有兩種方式:一種是創(chuàng)建Thread子類的一個(gè)實(shí)例并重寫run方法,第二種是創(chuàng)建類的時(shí)候?qū)崿F(xiàn)Runnable接口。接下來我們會(huì)具體講解這兩種方法:
創(chuàng)建Thread的子類
創(chuàng)建Thread子類的一個(gè)實(shí)例并重寫run方法,run方法會(huì)在調(diào)用start()方法之后被執(zhí)行。例子如下:
- public class MyThread extends Thread {
-
- public void run(){
- System.out.println("MyThread running");
- }
可以用如下方式創(chuàng)建并運(yùn)行上述Thread子類
- MyThread myThread = new MyThread();
- myTread.start();
一旦線程啟動(dòng)后start方法就會(huì)立即返回,而不會(huì)等待到run方法執(zhí)行完畢才返回。就好像run方法是在另外一個(gè)cpu上執(zhí)行一樣。當(dāng)run方法執(zhí)行后,將會(huì)打印出字符串MyThread running。
你也可以如下創(chuàng)建一個(gè)Thread的匿名子類:
- Thread thread = new Thread(){
- public void run(){
- System.out.println("Thread Running");
- }
- }
-
- thread.start();
當(dāng)新的線程的run方法執(zhí)行以后,計(jì)算機(jī)將會(huì)打印出字符串”Thread Running”。
實(shí)現(xiàn)Runnable接口
第二種編寫線程執(zhí)行代碼的方式是新建一個(gè)實(shí)現(xiàn)了java.lang.Runnable接口的類的實(shí)例,實(shí)例中的方法可以被線程調(diào)用。下面給出例子:
- public class MyRunnable implements Runnable {
-
- public void run(){
- System.out.println("MyRunnable running");
- }
- }
為了使線程能夠執(zhí)行run()方法,需要在Thread類的構(gòu)造函數(shù)中傳入 MyRunnable的實(shí)例對(duì)象。示例如下:
- Thread thread = new Thread(new MyRunnable());
- thread.start();
當(dāng)線程運(yùn)行時(shí),它將會(huì)調(diào)用實(shí)現(xiàn)了Runnable接口的run方法。上例中將會(huì)打印出”MyRunnable running”。
同樣,也可以創(chuàng)建一個(gè)實(shí)現(xiàn)了Runnable接口的匿名類,如下所示:
- Runnable myRunnable = new Runnable(){
-
- public void run(){
- System.out.println("Runnable running");
- }
- }
-
-
- Thread thread = new Thread(myRunnable);
- thread.start();
創(chuàng)建子類還是實(shí)現(xiàn)Runnable接口?
對(duì)于這兩種方式哪種好并沒有一個(gè)確定的答案,它們都能滿足要求。就我個(gè)人意見,我更傾向于實(shí)現(xiàn)Runnable接口這種方法。因?yàn)榫€程池可以有效的
管理實(shí)現(xiàn)了Runnable接口的線程,如果線程池滿了,新的線程就會(huì)排隊(duì)等候執(zhí)行,直到線程池空閑出來為止。而如果線程是通過實(shí)現(xiàn)Thread子類實(shí)現(xiàn)
的,這將會(huì)復(fù)雜一些。
有時(shí)我們要同時(shí)融合實(shí)現(xiàn)Runnable接口和Thread子類兩種方式。例如,實(shí)現(xiàn)了Thread子類的實(shí)例可以執(zhí)行多個(gè)實(shí)現(xiàn)了Runnable接口的線程。一個(gè)典型的應(yīng)用就是線程池。
常見錯(cuò)誤:調(diào)用run()方法而非start()方法
創(chuàng)建并運(yùn)行一個(gè)線程所犯的常見錯(cuò)誤是調(diào)用線程的run()方法而非start()方法,如下所示:
- Thread newThread = new Thread(MyRunnable());
- thread.run();
起初你并不會(huì)感覺到有什么不妥,因?yàn)閞un()方法的確如你所愿的被調(diào)用了。但是,事實(shí)上,run()方法并非是由剛創(chuàng)建的新線程所執(zhí)行的,而是被
創(chuàng)建新線程的當(dāng)前線程所執(zhí)行了。也就是被執(zhí)行上面兩行代碼的線程所執(zhí)行的。想要讓創(chuàng)建的新線程執(zhí)行run()方法,必須調(diào)用新線程的start方法。
線程名
當(dāng)創(chuàng)建一個(gè)線程的時(shí)候,可以給線程起一個(gè)名字。它有助于我們區(qū)分不同的線程。例如:如果有多個(gè)線程寫入System.out,我們就能夠通過線程名容易的找出是哪個(gè)線程正在輸出。例子如下:
- MyRunnable runnable = new MyRunnable();
- Thread thread = new Thread(runnable, "New Thread");
-
- thread.start();
- System.out.println(thread.getName());
需要注意的是,因?yàn)镸yRunnable并非Thread的子類,所以MyRunnable類并沒有g(shù)etName()方法??梢酝ㄟ^以下方式得到當(dāng)前線程的引用:
因此,通過如下代碼可以得到當(dāng)前線程的名字:
- String threadName = Thread.currentThread().getName();
線程代碼舉例:
這里是一個(gè)小小的例子。首先輸出執(zhí)行main()方法線程名字。這個(gè)線程由JVM分配的。然后開啟10個(gè)線程,命名為1~10。每個(gè)線程輸出自己的名字后就退出。
- public class ThreadExample {
-
- public static void main(String[] args){
- System.out.println(Thread.currentThread().getName());
- for(int i=0; i<10; i++){
- new Thread("" + i){
- public void run(){
- System.out.println("Thread: " + getName() + " running");
- }
- }.start();
- }
- }
- }
需要注意的是,盡管啟動(dòng)線程的順序是有序的,但是執(zhí)行的順序并非是有序的。也就是說,1號(hào)線程并不一定是第一個(gè)將自己名字輸出到控制臺(tái)的線程。這是因?yàn)榫€程是并行執(zhí)行而非順序的。Jvm和操作系統(tǒng)一起決定了線程的執(zhí)行順序,它和線程的啟動(dòng)順序并非一定是一致的。
競(jìng)態(tài)條件與臨界區(qū)
在同一程序中運(yùn)行多個(gè)線程本身不會(huì)導(dǎo)致問題,問題在于多個(gè)線程訪問了相同的資源。如同一內(nèi)存區(qū)(變量,數(shù)組,或?qū)ο螅?、系統(tǒng)(數(shù)據(jù)庫(kù),web
services等)或文件。實(shí)際上,這些問題只有在一或多個(gè)線程向這些資源做了寫操作時(shí)才有可能發(fā)生,只要資源沒有發(fā)生變化,多個(gè)線程讀取相同的資源就
是安全的。
多線程同時(shí)執(zhí)行下面的代碼可能會(huì)出錯(cuò):
- public class Counter {
-
- protected long count = 0;
-
- public void add(long value){
- this.count = this.count + value;
- }
- }
想象下線程A和B同時(shí)執(zhí)行同一個(gè)Counter對(duì)象的add()方法,我們無法知道操作系統(tǒng)何時(shí)會(huì)在兩個(gè)線程之間切換。JVM并不是將這段代碼視為單條指令來執(zhí)行的,而是按照下面的順序:
- 從內(nèi)存獲取 this.count 的值放到寄存器
- 將寄存器中的值增加value
- 將寄存器中的值寫回內(nèi)存
觀察線程A和B交錯(cuò)執(zhí)行會(huì)發(fā)生什么:
- this.count = 0;
- A: 讀取 this.count 到一個(gè)寄存器 (0)
- B: 讀取 this.count 到一個(gè)寄存器 (0)
- B: 將寄存器的值加2
- B: 回寫寄存器值(2)到內(nèi)存. this.count 現(xiàn)在等于 2
- A: 將寄存器的值加3
- A: 回寫寄存器值(3)到內(nèi)存. this.count 現(xiàn)在等于 3
兩個(gè)線程分別加了2和3到count變量上,兩個(gè)線程執(zhí)行結(jié)束后count變量的值應(yīng)該等于5。然而由于兩個(gè)線程是交叉執(zhí)行的,兩個(gè)線程從內(nèi)存中讀
出的初始值都是0。然后各自加了2和3,并分別寫回內(nèi)存。最終的值并不是期望的5,而是最后寫回內(nèi)存的那個(gè)線程的值,上面例子中最后寫回內(nèi)存的是線程A,
但實(shí)際中也可能是線程B。如果沒有采用合適的同步機(jī)制,線程間的交叉執(zhí)行情況就無法預(yù)料。
競(jìng)態(tài)條件 & 臨界區(qū)
當(dāng)兩個(gè)線程競(jìng)爭(zhēng)同一資源時(shí),如果對(duì)資源的訪問順序敏感,就稱存在競(jìng)態(tài)條件。導(dǎo)致競(jìng)態(tài)條件發(fā)生的代碼區(qū)稱作臨界區(qū)。上例中add()方法就是一個(gè)臨界區(qū),它會(huì)產(chǎn)生競(jìng)態(tài)條件。在臨界區(qū)中使用適當(dāng)?shù)耐骄涂梢员苊飧?jìng)態(tài)條件。
線程安全與共享資源
允許被多個(gè)線程同時(shí)執(zhí)行的代碼稱作線程安全的代碼。線程安全的代碼不包含競(jìng)態(tài)條件。當(dāng)多個(gè)線程同時(shí)更新共享資源時(shí)會(huì)引發(fā)競(jìng)態(tài)條件。因此,了解Java線程執(zhí)行時(shí)共享了什么資源很重要。
局部變量
局部變量存儲(chǔ)在線程自己的棧中。也就是說,局部變量永遠(yuǎn)也不會(huì)被多個(gè)線程共享。所以,基礎(chǔ)類型的局部變量是線程安全的。下面是基礎(chǔ)類型的局部變量的一個(gè)例子:
- public void someMethod(){
- long threadSafeInt = 0;
- threadSafeInt++;
- }
局部的對(duì)象引用
對(duì)象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對(duì)象并沒有存儲(chǔ)在線程的棧內(nèi)。所有的對(duì)象都存儲(chǔ)在共享堆中。如果在某個(gè)方法中創(chuàng)建的對(duì)象不會(huì)逃逸出(譯者注:即該對(duì)象不會(huì)被其它方法獲得,也不會(huì)被非局部變量引用到)該方法,那么它就是線程安全的。實(shí)際上,哪怕將這個(gè)對(duì)象作為參數(shù)傳給其它方法,只要?jiǎng)e的線程獲取不到這個(gè)對(duì)象,那它仍是線程安全的。下面是一個(gè)線程安全的局部引用樣例:
- public void someMethod(){
-
- LocalObject localObject = new LocalObject();
-
- localObject.callMethod();
- method2(localObject);
- }
-
- public void method2(LocalObject localObject){
- localObject.setValue("value");
- }
樣例中LocalObject對(duì)象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對(duì)象。每個(gè)執(zhí)行someMethod()的線程
都會(huì)創(chuàng)建自己的LocalObject對(duì)象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實(shí)上,整個(gè)
someMethod()都是線程安全的。即使將LocalObject作為參數(shù)傳給同一個(gè)類的其它方法或其它類的方法時(shí),它仍然是線程安全的。當(dāng)然,如
果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了。
對(duì)象成員
對(duì)象成員存儲(chǔ)在堆上。如果兩個(gè)線程同時(shí)更新同一個(gè)對(duì)象的同一個(gè)成員,那這個(gè)代碼就不是線程安全的。下面是一個(gè)樣例:
- public class NotThreadSafe{
- StringBuilder builder = new StringBuilder();
-
- public add(String text){
- this.builder.append(text);
- }
- }
如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)NotThreadSafe 實(shí)例上的add()方法,就會(huì)有競(jìng)態(tài)條件問題。例如:
- NotThreadSafe sharedInstance = new NotThreadSafe();
-
- new Thread(new MyRunnable(sharedInstance)).start();
- new Thread(new MyRunnable(sharedInstance)).start();
-
- public class MyRunnable implements Runnable{
- NotThreadSafe instance = null;
-
- public MyRunnable(NotThreadSafe instance){
- this.instance = instance;
- }
-
- public void run(){
- this.instance.add("some text");
- }
- }
注意兩個(gè)MyRunnable共享了同一個(gè)NotThreadSafe對(duì)象。因此,當(dāng)它們調(diào)用add()方法時(shí)會(huì)造成競(jìng)態(tài)條件。
當(dāng)然,如果這兩個(gè)線程在不同的NotThreadSafe實(shí)例上調(diào)用call()方法,就不會(huì)導(dǎo)致競(jìng)態(tài)條件。下面是稍微修改后的例子:
- new Thread(new MyRunnable(new NotThreadSafe())).start();
- new Thread(new MyRunnable(new NotThreadSafe())).start();
現(xiàn)在兩個(gè)線程都有自己?jiǎn)为?dú)的NotThreadSafe對(duì)象,調(diào)用add()方法時(shí)就會(huì)互不干擾,再也不會(huì)有競(jìng)態(tài)條件問題了。所以非線程安全的對(duì)象仍可以通過某種方式來消除競(jìng)態(tài)條件。
線程控制逃逸規(guī)則
線程控制逃逸規(guī)則可以幫助你判斷代碼中對(duì)某些資源的訪問是否是線程安全的。
- <span style="color:#FF0000;"><strong>如果一個(gè)資源的創(chuàng)建,使用,銷毀都在同一個(gè)線程內(nèi)完成,
- 且永遠(yuǎn)不會(huì)脫離該線程的控制,則該資源的使用就是線程安全的。</strong></span>
資源可以是對(duì)象,數(shù)組,文件,數(shù)據(jù)庫(kù)連接,套接字等等。Java中你無需主動(dòng)銷毀對(duì)象,所以“銷毀”指不再有引用指向?qū)ο蟆?/p>
即使對(duì)象本身線程安全,但如果該對(duì)象中包含其他資源(文件,數(shù)據(jù)庫(kù)連接),整個(gè)應(yīng)用也許就不再是線程安全的了。比如2個(gè)線程都創(chuàng)建了各自的數(shù)據(jù)庫(kù)連接,每個(gè)連接自身是線程安全的,但它們所連接到的同一個(gè)數(shù)據(jù)庫(kù)也許不是線程安全的。比如,2個(gè)線程執(zhí)行如下代碼:
如果兩個(gè)線程同時(shí)執(zhí)行,而且碰巧檢查的是同一個(gè)記錄,那么兩個(gè)線程最終可能都插入了記錄:
- 線程1檢查記錄X是否存在。檢查結(jié)果:不存在
- 線程2檢查記錄X是否存在。檢查結(jié)果:不存在
- 線程1插入記錄X
- 線程2插入記錄X
同樣的問題也會(huì)發(fā)生在文件或其他共享資源上。因此,區(qū)分某個(gè)線程控制的對(duì)象是資源本身,還是僅僅到某個(gè)資源的引用很重要。
總結(jié):
線程安全與不可變性
當(dāng)多個(gè)線程同時(shí)訪問同一個(gè)資源,并且其中的一個(gè)或者多個(gè)線程對(duì)這個(gè)資源進(jìn)行了寫操作,才會(huì)產(chǎn)生競(jìng)態(tài)條件。多個(gè)線程同時(shí)讀同一個(gè)資源不會(huì)產(chǎn)生競(jìng)態(tài)條件。
我們可以通過創(chuàng)建不可變的共享對(duì)象來保證對(duì)象在線程間共享時(shí)不會(huì)被修改,從而實(shí)現(xiàn)線程安全。如下示例:
- public class ImmutableValue{
-
- private int value = 0;
-
- public ImmutableValue(int value){
- this.value = value;
- }
-
- public int getValue(){
- return this.value;
- }
- }
請(qǐng)注意ImmutableValue類的成員變量value 是通過構(gòu)造函數(shù)賦值的,并且在類中沒有set方法。這意味著一旦ImmutableValue實(shí)例被創(chuàng)建,value 變量就不能再被修改,這就是不可變性。但你可以通過getValue()方法讀取這個(gè)變量的值。
(譯者注:注意,“不變”(Immutable)和“只讀”(Read
Only)是不同的。當(dāng)一個(gè)變量是“只讀”時(shí),變量的值不能直接改變,但是可以在其它變量發(fā)生改變的時(shí)候發(fā)生改變。比如,一個(gè)人的出生年月日是“不變”屬
性,而一個(gè)人的年齡便是“只讀”屬性,但是不是“不變”屬性。隨著時(shí)間的變化,一個(gè)人的年齡會(huì)隨之發(fā)生變化,而一個(gè)人的出生年月日則不會(huì)變化。這就是“不
變”和“只讀”的區(qū)別。(摘自《Java與模式》第34章))
如果你需要對(duì)ImmutableValue類的實(shí)例進(jìn)行操作,可以通過得到value變量后創(chuàng)建一個(gè)新的實(shí)例來實(shí)現(xiàn),下面是一個(gè)對(duì)value變量進(jìn)行加法操作的示例:
- public class ImmutableValue{
-
- private int value = 0;
-
- public ImmutableValue(int value){
- this.value = value;
- }
-
- public int getValue(){
- return this.value;
- }
-
- public ImmutableValue add(int valueToAdd){
- return new ImmutableValue(this.value + valueToAdd);
- }
-
- }
請(qǐng)注意add()方法以加法操作的結(jié)果作為一個(gè)新的ImmutableValue類實(shí)例返回,而不是直接對(duì)它自己的value變量進(jìn)行操作。
引用不是線程安全的!
重要的是要記住,即使一個(gè)對(duì)象是線程安全的不可變對(duì)象,指向這個(gè)對(duì)象的引用也可能不是線程安全的。看這個(gè)例子:
- public void Calculator{
- private ImmutableValue currentValue = null;
-
- public ImmutableValue getValue(){
- return currentValue;
- }
-
- public void setValue(ImmutableValue newValue){
- this.currentValue = newValue;
- }
-
- public void add(int newValue){
- this.currentValue = this.currentValue.add(newValue);
- }
- }
Calculator類持有一個(gè)指向ImmutableValue實(shí)例的引用。注意,通過setValue()方法和add()方法可能會(huì)改變這個(gè)
引用。因此,即使Calculator類內(nèi)部使用了一個(gè)不可變對(duì)象,但Calculator類本身還是可變的,因此Calculator類不是線程安全
的。換句話說:ImmutableValue類是線程安全的,但使用它的類不是。當(dāng)嘗試通過不可變性去獲得線程安全時(shí),這點(diǎn)是需要牢記的。
要使Calculator類實(shí)現(xiàn)線程安全,將getValue()、setValue()和add()方法都聲明為同步方法即可。
|
1. 局部變量中的基本數(shù)據(jù)類型(8種)永遠(yuǎn)是線程安全的。
2. 局部變量中的對(duì)象類型只要不會(huì)被其他線程訪問到,也是線程安全的。
3. 一個(gè)對(duì)象實(shí)例被多個(gè)線程同時(shí)訪問時(shí),他的成員變量就可能是線程不安全的。