1Java多線程技能
??? 講到多線程技術(shù)狱庇,我們就不得不提及“進程”和“線程”的概念粉臊,“百度百科”里對“進程”的解釋如下:進程是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ)箫爷;是一次程序的執(zhí)行舌稀;是一個程序及其數(shù)據(jù)在處理機上順序執(zhí)行時鎖發(fā)生的活動场绿;是程序在一個數(shù)據(jù)集合上運行的過程剖效,它是系統(tǒng)進行資源匹配和調(diào)度的一個獨立單位。通俗的來講其實就是一個正在操作系統(tǒng)中運行的exe程序,如圖1-1所示璧尸,而線程可以理解成是在進程中獨立運行的子任務(wù)咒林。比如,QQ.exe運行時就有很多的子任務(wù)在同時運行爷光。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? 圖1-1系統(tǒng)進程列表
??? 那么使用多線程有什么優(yōu)點呢垫竞?如果有使用過“多任務(wù)操作系統(tǒng)”的經(jīng)驗,比如Windows蛀序,那么我們對于它的方便性就會有深入的體會:使用多任務(wù)操作系統(tǒng)后欢瞪,可以最大限度的利用CPU的空閑時間來處理其他任務(wù),比如一邊處理正在由打印機打印數(shù)據(jù)徐裸,一邊使用Word編輯文檔遣鼓。為了更好的理解多線程的優(yōu)勢,我們可以看一些多線程模型(圖1-2)和單線程模型(圖1-3)重贺,總的來說多線程可以使CPU異步執(zhí)行骑祟,大大的提高CPU的利用率,而單線程只能順序的執(zhí)行气笙。
??????????????????????????????????????????????????????????????? 圖1-2多任務(wù)環(huán)境
??????????????????????????????????????????????????????????????? 圖1-3單任務(wù)環(huán)境
??? 講了線程的概念以及多線程的優(yōu)點曾我,接下來我們來談一談怎么使用多線程。首先是實現(xiàn)多線程健民,一般實現(xiàn)多線程的方式主要有兩種抒巢,一種是繼承Thread類,另一種是實現(xiàn)Runnable接口秉犹,通過Java的源碼可以發(fā)現(xiàn)蛉谜,Thread類實現(xiàn)了Runnable接口,同時又考慮到Java不支持多繼承崇堵,所以通過實現(xiàn)Runnable接口的方式來實現(xiàn)多線程更加符合Java的特性型诚;其次是啟動線程,啟動線程一般有start()方法和run()方法鸳劳,start()方法會通知“線程規(guī)劃器”此線程已經(jīng)準備就緒狰贯,等待系統(tǒng)安排一個時間來調(diào)用Thread中run()方法,由于“線程規(guī)劃器”調(diào)用的隨機性赏廓,所有執(zhí)行start()的順序不代表線程啟動的順序涵紊,具有異步性;而run()方法卻不會交給“線程規(guī)劃器”而是交給main主線程來調(diào)用run()方法幔摸,也就是必須等run()方法執(zhí)行完成后摸柄,才能執(zhí)行后面的代碼,具有順序性既忆;接著是暫停/恢復(fù)線程驱负,Java中一般用suspend()方法和resume()方法來暫停/恢復(fù)線程嗦玖,但是suspend和resume的缺點是獨占,如果使用不當(dāng)?shù)脑捲炯梗瑯O易造成公共對象的獨占宇挫,是的其他線程無法訪問公共同步對象,同時suspend和resume容易應(yīng)為線程的暫停導(dǎo)致數(shù)據(jù)的不同步酪术;最后是停止線程器瘪,一種是直接使用stop()方法,但是這個方法不安全拼缝、會拋出ThreadDeath異常娱局、強制讓線程停止導(dǎo)致一些請理性工作得不到完成而且會對鎖定的對象進行“解鎖”導(dǎo)致數(shù)據(jù)不同步,而且是已被啟用作廢的咧七,在將來的Java版本中衰齐,這個方法將不可用或不支持,還有一種是通過interrupt()方法继阻,給線程注冊一個中斷標(biāo)識耻涛,然后通過interrupted()和isInterrupted()方法來判斷是否有中斷標(biāo)識,然后結(jié)合拋異常瘟檩、break或者return來退出線程抹缕,對于interrupted()方法是用來測試當(dāng)前線程是否已經(jīng)中斷,具有清除中斷標(biāo)識的作用墨辛,isInterrupted()方法用來測試是否已經(jīng)中斷卓研,不具有清除中標(biāo)功能。
??? 在Java線程中有兩種線程睹簇,一種是用戶線程奏赘,另一種是守護線程。守護線程時特殊的線程太惠,它的特性有“陪伴”的含義磨淌,當(dāng)進程中不存在非守護線程了,則守護線程自動銷毀凿渊,典型的守護線程就是垃圾回收梁只,主要是為其他線程的運行的運行提供服務(wù),用戶可以通過Thread.setDameon()方法將一個線程設(shè)置為守護線程埃脏。
2 對象及變量的并發(fā)訪問
??? 前面講述了多線程的技能搪锣,從而我們對線程的創(chuàng)建、啟動剂癌、暫停淤翔、停止等基本操作有了一定的了解,接下來當(dāng)然就會涉及到線程中的實例變量佩谷、局部變量以及方法的訪問控制的問題旁壮,對于自定義線程中實例變量存在有兩種情況,一種是不共享數(shù)據(jù)的情況(如圖2-1)谐檀,一種是共享數(shù)據(jù)的情況(如圖2-2)抡谐,對于不共享數(shù)據(jù)的情況下,線程之間就不會存在“非線程安全”問題桐猬,而對于共享數(shù)據(jù)的情況下就會存在“非線程安全”問題麦撵,例如:對于一個i--操作來說,在某些JVM中要分成如下三步:
?1) 取得原有i值
?2) 計算i--
?3)? 對i進行賦值
在這3個步驟中溃肪,如果有多個線程同時訪問免胃,那么就會出現(xiàn)“非線程安全”問題。
??????????????????????????????????????????????????????????????? 圖2-1不共享數(shù)據(jù)
??????????????????????????????????????????????????????????????????? 圖2-2共享數(shù)據(jù)
??? 對于“非線程安全”問題常用的處理方法是在方法前加上synchronized關(guān)鍵字惫撰,使多個線程以排隊的順序執(zhí)行某一個方法羔沙。當(dāng)一個線程調(diào)用方法前,先判斷當(dāng)前方法有沒有上鎖厨钻,如果上鎖了扼雏,說明有其他的線程正在調(diào)用此方法,必須等到其他線程結(jié)束后才可以執(zhí)行此方法夯膀,這樣就實現(xiàn)了排隊調(diào)用方法的目的了诗充。但有一點我們必須要搞清楚,對于非靜態(tài)方法前加入synchronized關(guān)鍵字诱建,關(guān)鍵字synchronized取得的鎖是對象鎖蝴蜓,而不是把一段代碼或者方法當(dāng)做鎖,如果多個線程訪問的是同一個對象那么加上synchronized關(guān)鍵字是可以解決“非線程安全”問題俺猿,但是如果是多個對象多個鎖茎匠,那么線程之間仍然是異步執(zhí)行的;對于靜態(tài)方法前加入synchronized關(guān)鍵字辜荠,關(guān)鍵字synchronized取得的鎖就是當(dāng)前的*.java文件的Class類汽抚,Class鎖可以對類的所有對象起作用。
然而使用關(guān)鍵字synchronized聲明方法在某些情況下是存在弊端的伯病,比如A線程調(diào)用同步方法執(zhí)行一個長時間的任務(wù)造烁,那么B線程則必須等待比較長的時間。在這樣的情況下可以使用synchronized同步語句塊來來解決午笛,只對需要順序執(zhí)行的某個或者某些語句進行加鎖惭蟋,從而提高運行效率,常用的同步語句方法有synchronized(this)药磺、synchronized(非this對象)告组、synchronized(class)。對于synchronized(this)和synchronized(非this對象)來說癌佩,監(jiān)視器對象都是圓括號中的所指定的對象木缝,而synchronized(class)的監(jiān)視器對象是類便锨,是與在靜態(tài)方法前加synchronized關(guān)鍵字具有同樣的概念。
??? 對于同一個類中多方法加了synchronized關(guān)鍵字或者synchronized語句塊的調(diào)用遵循以下規(guī)則:
???? 1)? A線程先持有Object對象的Lock鎖我碟,B線程可以以異步的方式調(diào)用Object對象中非synchronized類型或者非synchronized代碼塊的方法放案。
???? 2)? A線程先持有Object對象的Lock鎖,B線程如果在這時調(diào)用Object對象中synchronized類型或者synchronized代碼塊的方法則需等待矫俺,也就是同步吱殉。
??? 總而言之,我們要區(qū)分清楚監(jiān)視器對象是否相同厘托,如果相同那么對于方法的調(diào)用就要遵循以上的規(guī)則友雳,如果監(jiān)視器對象不同,那么線程之間的方法調(diào)用就是異步的铅匹,而且如果線程遇到異常后會自動的釋放掉鎖持有的鎖押赊,防止出現(xiàn)死鎖的現(xiàn)象。
??? 在Java5中引入了一種Lock對象伊群,該對象也能實現(xiàn)同步的效果考杉,并且在擴張功能上也更加強大,比如具有嗅探鎖定舰始、多路通知等功能崇棠。而且使用上比synchronized更加的靈,同時類ReentrantLock具有完全互斥排他的效果丸卷,即同一個時間只有一個線程在執(zhí)行ReentrantLock.lock()方法后面的任務(wù)枕稀,這樣做雖然保證了實例變量的線程安全,但是效率卻是非常低下谜嫉。所以在JDK中提供了一種讀寫鎖ReentrantReadWriteLock類萎坷,使用它可以加快運行效率。讀寫鎖表示也有兩個鎖沐兰,一個是讀操作相關(guān)的鎖哆档,也叫共享鎖;另一個是寫操作相關(guān)的鎖住闯,也好排它鎖瓜浸。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥比原,寫鎖與寫鎖互斥插佛。
3 線程間通信
??? 線程是操作系統(tǒng)中獨立的個體,但這些個體如果不經(jīng)過特殊的處理就不能成為一個整體量窘。線程間通信就是成為整體的備用方案之一雇寇,可以說,使線程進行通信后,系統(tǒng)之間的交互性會更強大锨侯,在大大提高CPU利用率的同時還會使程序員對給線程任務(wù)處理的過程中進行有效地把控與監(jiān)督嫩海。
??? 通過不斷的使用while語句輪詢機制來檢測某一個條件,來進行線程間的通信识腿,這樣會浪費CPU資源出革,如果輪詢的時間間隔很小造壮,更浪費CPU資源渡讼;如果輪詢的時間間隔很大,有可能會取不到想要得到的數(shù)據(jù)耳璧。所以就需要有一種機制來實現(xiàn)減少CPU的資源浪費成箫。而且還可以實現(xiàn)在多個線程間通信,他就是“wait/notify”機制旨枯。等待/通知機制在生活中比比皆是蹬昌,比如在就餐中就會出現(xiàn),如圖3-1所示攀隔。
??? 廚師和服務(wù)員之間的交互要在“菜品傳遞臺”上皂贩,在這期間會有幾個問題:
???? 1)? 廚師做完一道菜的時間不確定,所以廚師將菜品放到“菜品傳遞臺”上的時間也不確定
???? 2)? ]服務(wù)員取到菜的時間取決于廚師昆汹,所以服務(wù)員就有“等待”(wait)的狀態(tài)明刷。
???? 3)? 服務(wù)員如何能取到菜呢?這又取決于廚師满粗,廚師將菜放在“菜品傳遞臺”上辈末,其實就相當(dāng)于一種通知(notify),這時服務(wù)員才可以拿到菜并交給就餐者映皆。
???? 4)? 在這個過程中出現(xiàn)了“等待/通知”機制挤聘。
??????????????????????????????????????????????????????????? 圖3-1就餐時出現(xiàn)等待通知
??? 方法wait()的作用是使得當(dāng)前執(zhí)行代碼的線程立刻進行等待,將當(dāng)前線程置入“預(yù)執(zhí)行隊列”中捅彻,并且在wait()所在的代碼行處停止執(zhí)行组去,同時會把自己所持有的鎖進行釋放,直到接到通知或被中斷為止步淹。在調(diào)用wait()之前从隆,線程必須獲得對象的對象級別鎖,即只能在同步方法或者同步代碼塊中調(diào)用wait()方法贤旷。方法notify()的主要作用是用來通知那些可能等待該對象鎖的其他線程广料,如果喲多個線程等待,則由“線程規(guī)劃器”隨機挑選出一個呈wait狀態(tài)的線程幼驶,并對其發(fā)出notify通知艾杏,并使其獲得該對象的對象鎖,同時notify也要在同步方法或者同步代碼塊中進行調(diào)用盅藻,即在調(diào)用前购桑,線程也必須獲得該對象的對象級別鎖畅铭。有一點需要明白的是味廊,在執(zhí)行notify()方法后医窿,當(dāng)前線程不會馬上釋放該對象鎖,呈wait狀態(tài)的線程也不能馬上獲取該對象鎖躯肌,要等到執(zhí)行notify()方法的線程將程序執(zhí)行完缭贡,也就是退出synchronized代碼塊后炉擅,當(dāng)前線程才會釋放鎖,而呈wait狀態(tài)所在的線程才可以獲得該對象鎖阳惹。方法notifyAll()可以使所有正在等待隊列中等待同一共享資源的“全部”線程從等待隊列退出谍失,進入可運行狀態(tài)。此時莹汤,只有第一個獲取到該對象鎖的線程才會進行running快鱼,如圖3-2所示中線程狀態(tài)切換示意圖:
??????????????????????????????????????????????????????????? 圖3-2線程狀態(tài)切換示意圖
??? 1) 新創(chuàng)建一個新的線程對象后,再調(diào)用它的start()方法纲岭,系統(tǒng)會為此線程分配CPU資源抹竹,時期處于Runnable(可運行)狀態(tài),這是一個準備運行的階段止潮,如果線程搶占到CPU資源窃判,此線程就處于Running(運行)狀態(tài)。
??? 2) Runnable狀態(tài)和Running狀態(tài)可以相互切換沽翔,因為有可能線程運行一段時間后兢孝,有其他高優(yōu)先級的線程搶占了CPU資源,這時此線程就從Running狀態(tài)變成Runnable狀態(tài)仅偎。
??? 3) Blocked是阻塞的意思跨蟹,例如遇到一個IO操作,此時CPU處于空閑狀態(tài)橘沥,可能會轉(zhuǎn)而把CPU時間片分配給其他線程窗轩,這時也可以成為“暫停”狀態(tài)座咆。Blocked結(jié)束后痢艺,進入Runnable狀態(tài),等待系統(tǒng)重新分配資源介陶。
?? 4) run()方法運行結(jié)束后進入銷毀階段堤舒,這個線程執(zhí)行完畢。
??? 在前面介紹過通過Lock對象也能實現(xiàn)同步的效果哺呜,并且在擴張功能上也更加強大舌缤,比如具有嗅探鎖定、多路通知等功能。而且使用上比synchronized更加的靈活国撵。關(guān)鍵字synchronized與wait()和notify()/notifyAll()方法結(jié)合可以實現(xiàn)通知/等待模式陵吸,但是使用notify()/notifyAll()方法進行通知時,被通知的線程卻是JVM隨機選擇的介牙,因為synchronized就相當(dāng)于整個Lock對象中只有一個單一的Condition對象壮虫,所以的線程都注冊在它一個對象的身上,而且對于notifyAll()時环础,需要通知所有的WAITTING線程囚似,沒有選擇權(quán),會出現(xiàn)相當(dāng)大的效率問題喳整。但是Lock對象里面可以創(chuàng)建多個Condition(即對象監(jiān)視器)實例谆构,線程可以注冊在指定的Condition中,從而可以有選擇性的進行線程通知框都,在調(diào)度線程上更加靈活。
??? Object類中的wait()方法相當(dāng)于Condition類中的await()方法呵晨。
??? Object類中的wait(long timeout)方法相當(dāng)于Condition類中的await(long timeout魏保,TimeUnit unit)方法。
??? Object類中的notify()方法相當(dāng)于Condition類中的signal()方法摸屠。
??? Object類中的notifyAll()方法相當(dāng)于Condition類中的signalAll()方法谓罗。
4 總結(jié)
??? 這本書主要講述了Java中多線程技術(shù),主要圍繞三個方面來進行講解季二,第一個方面講的主要就是一些對線程的基本操作檩咱,使我們對線程能有一個初步的認識,知道可以通過繼承Thread類或者實現(xiàn)Runnable接口來實現(xiàn)一個自定義的線程類胯舷,同時明白這兩種方法的區(qū)別以及JDK中關(guān)于Thread的API的使用刻蚯;第二個方面主要講解的是怎么對變量以及方法的訪問進行控制,保證不會出現(xiàn)一些“非線程安全”的問題桑嘶,主要提到的解決方案有synchronized關(guān)鍵字炊汹、synchronized代碼塊、Lock類等來控制線程對實例變量和方法的順序訪問逃顶,同時知道方法中的變量不會出現(xiàn)“非線程安全”問題讨便;第三方面講的是怎么把一個個單獨的線程結(jié)合成一個整體,使得線程之間可以相互的通信以政,從而保證CPU具有高效的利用率霸褒,在這方面介紹的主要技術(shù)就是wait/notifhe lock/condition,并指出lock/condition比wait/notify方法具有更高的效率以及靈活性盈蛮,因為lock/condition可以注冊多個監(jiān)視器對象废菱,將不同的對象注冊到不同的監(jiān)視器對象上,可以更加方便的對線程進行管理而wait/notify只有一個監(jiān)視器對象。