陳益 王佩
摘要:為了避免Java應(yīng)用程序中多個線程共享同一個資源時產(chǎn)生訪問沖突,確保線程安全,采用同步機(jī)制為每個線程合理地分配訪問資源。編寫一個模擬火車站售票過程的Java應(yīng)用程序,由4個線程完成100張火車票的出售,調(diào)用sleep方法查看非同步環(huán)境下每個線程訪問資源的狀況。分析多線程采用同步機(jī)制和非同步機(jī)制的實(shí)驗(yàn)給系統(tǒng)帶來的影響。實(shí)驗(yàn)證明,借助同步機(jī)制能合理地為每個線程提供沒有任何沖突的資源訪問,使Java多線程程序獲得更好的健壯性。
關(guān)鍵詞:Java多線程;訪問資源;線程安全;同步機(jī)制;健壯性
Java Multi?thread Safety Problems Application Analysis Based?on Synchronized Mechanism
CHEN Yi, WANG Pei
(School of Science, Hubei University of Technology, Wuhan 430068,China)
Abstract:In order to avoid access conflicts when multiple threads in a Java application share the same resource and ensure thread safety, a synchronized mechanism is used to reasonably allocate access resources for each thread. We write a Java application that simulates the ticket sales process at the train station. The sale of 100 train tickets are completed by 4 threads, and the sleep method is called to check the status of each thread accessing resources in an asynchronous environment. The impacts of the experimental results of multi?threaded synchronization and non?synchronization mechanisms on the system are analyzed. Experiments show that the synchronization mechanism can reasonably provide each thread resource access without conflicts, which makes Java multi?threaded program execution more robust.
Key Words:Java multi?thread;access to resources;thread safety;synchronization mechanism;robustness
0?引言
Java是一種在語言級提供支持多線程程序設(shè)計(jì)的編程語言。Java多線程表現(xiàn)靈活、應(yīng)用復(fù)雜、不易掌握。處理不好,不僅不能發(fā)揮其優(yōu)勢,還會引發(fā)一些線程安全方面的問題,線程安全是由CPU控制多個線程對某個資源的有序訪問。多線程安全訪問過程指當(dāng)一個線程訪問該類某個數(shù)據(jù)時,需加鎖進(jìn)行保護(hù)使其它線程不能訪問,直至該線程讀取完成,以免出現(xiàn)數(shù)據(jù)不一致或數(shù)據(jù)污染[1]。Hashtable引入多線程安全機(jī)制,因本身并沒有實(shí)現(xiàn)序列化接口,所以多線程機(jī)制中線程并不安全。多線程的安全訪問必須借用同步(synchronized)加鎖機(jī)制,確保線程間訪問安全[2]。
單線程只有一條從頭至尾的執(zhí)行線索,CPU資源利用率不夠充分,多線程最大限度地利用CPU資源,當(dāng)某一線程的處理不需要占用CPU資源時,可以讓其它線程有機(jī)會獲取CPU資源,節(jié)省時間,提高系統(tǒng)執(zhí)行效率[3]。多線程還可將任務(wù)分塊后同時執(zhí)行,效率更高、利用率更優(yōu)、程序更簡單、響應(yīng)速度更快。
1?多線程概念、Java多線程創(chuàng)建方式與同步機(jī)制
線程最早出現(xiàn)在操作系統(tǒng)中,是程序的一個執(zhí)行流,稱為輕量級進(jìn)程,由操作系統(tǒng)調(diào)度。任何一個Java應(yīng)用程序至少有一個單線程,稱為main主線程[4]。多線程是實(shí)現(xiàn)并發(fā)機(jī)制的一種手段,指一個程序包括多個執(zhí)行流,多個線程共享一個進(jìn)程存儲空間。
Java中創(chuàng)建多線程主要有兩種方式:繼承自Thread類和實(shí)現(xiàn)Runnable接口。Java語言引入包的概念,方便了類的繼承和接口的實(shí)現(xiàn),Thread類和Runnable接口來自于Java.lang包,是Java API中唯一一個不需要用戶導(dǎo)入便可以直接使用其中的類和接口的包[5]。
繼承Thread創(chuàng)建一個新的線程,首先用關(guān)鍵字class聲明一個類,使其繼承Thread類,用new關(guān)鍵字創(chuàng)建一個類的對象,由對象調(diào)用Thread類中start方法,啟動創(chuàng)建的新線程,最后Java虛擬機(jī)(JVM)調(diào)用線程的run方法,用戶在run方法中的功能被執(zhí)行。如果把main方法稱為Java程序入口方法,則run方法為多線程入口方法。繼承Thread類創(chuàng)建多線程的方法簡單,但Java中只提供了對類的單繼承操作,若一個類已經(jīng)繼承了另一個類,便不能再繼承Thread類。用一個聲明好的類去實(shí)現(xiàn)Runnable接口是創(chuàng)建Java多線程的另一種方法[6]。Runnable接口中只有一個run方法,一個類去實(shí)現(xiàn)Runnable接口,必須在實(shí)現(xiàn)類中重新定義run方法。實(shí)現(xiàn)類中重新定義run方法的訪問權(quán)限不能低于Runnable接口中run方法的訪問權(quán)限,否則程序編譯會報(bào)錯。由實(shí)現(xiàn)了Runnable接口所創(chuàng)建多個線程的類,既可以繼承其它類,還能實(shí)現(xiàn)其它接口,每個接口之間用逗號進(jìn)行分隔,增強(qiáng)類的功能,在一個類中包容所有代碼,增加邏輯性,便于封裝[7]。
比較創(chuàng)建多線程的兩種方法,發(fā)現(xiàn)其行為一致。通常情況下,如不需要修改線程類中除了run方法之外的其它行為,一般用實(shí)現(xiàn)Runnable接口的方式創(chuàng)建新線程,實(shí)現(xiàn)Runnable接口對多個線程訪問同一個資源極為方便。本文模擬系統(tǒng)以實(shí)現(xiàn)Runnable接口使用Java多線程技術(shù)進(jìn)行操作。當(dāng)Java多個線程共享同一個資源時,在并發(fā)運(yùn)行過程中可能會同時訪問“臨界區(qū)”,為確保線程間訪問安全,必須用線程同步操作對“臨界區(qū)”共享資源一致性進(jìn)行維護(hù)。關(guān)鍵字同步有同步塊和同步方法兩種方式[8],保證了線程間同步。
2?多線程同步機(jī)制
2.1?訪問共享資源引發(fā)線程安全問題
假如有100張火車票可以出售,由4個售票窗口同時為旅客服務(wù),完成售票過程。4個售票窗口需要創(chuàng)建4個新線程,新線程的創(chuàng)建由一個類實(shí)現(xiàn)Runnable接口完成。一個類繼承自Thread類能創(chuàng)建新線程,就Thread類本身而言,它實(shí)現(xiàn)了Runnable接口,用戶可以將由實(shí)現(xiàn)類創(chuàng)建的對象作為Thread類的參數(shù)傳遞進(jìn)來,創(chuàng)建一個新線程,調(diào)用Thread類start方法啟動線程,如圖1中矩形標(biāo)注所示。編寫一個完整的Java應(yīng)用程序完成火車票售票工作模擬過程的源程序段如圖1所示,圖2是執(zhí)行結(jié)果的一部分。
由圖2的結(jié)果可以發(fā)現(xiàn),用戶在執(zhí)行程序后,所售票號不連續(xù)且呈無序狀態(tài),導(dǎo)致同一張票由多個線程同時出售,比如100、99張票都分別有多個線程出售,如圖2標(biāo)注所示,這是一個顯式的線程訪問安全問題。還有一個更致命的隱式問題:當(dāng)剩下最后1張票時,由于時間片的緣故,會打印0、 -1、-2等不正確的票據(jù)格式。因?yàn)?個線程共享同一個資源,引發(fā)了線程間訪問安全問題。4個線程共享同一個tickets變量的狀況,如圖1中橢圓形標(biāo)注所示,顯示的安全問題如圖2標(biāo)注所示。對于隱藏的訪問安全問題,用戶只需在源程序SellThread類run方法中調(diào)用sleep方法,讓執(zhí)行的線程睡眠10ms,所有的錯誤便能清晰地顯示出來,對sleep方法的調(diào)用會產(chǎn)生異常,需要用try/catch進(jìn)行處理,源代碼段如圖3矩形框內(nèi)標(biāo)注所示,執(zhí)行結(jié)果如圖4矩形框內(nèi)標(biāo)注所示。
由圖1和圖3源程序中的if代碼段可知,該代碼區(qū)域?yàn)椤芭R界區(qū)”,指在一個多線程程序中,單獨(dú)、并發(fā)的線程訪問代碼段中的同一資源,代碼段被稱為“臨界區(qū)”。當(dāng)多個線程共享同一個資源,方便的同時也存在訪問安全的風(fēng)險(xiǎn)。Java多線程利用同步機(jī)制協(xié)調(diào)管理“臨界區(qū)”,以確保線程訪問安全[9]。
2.2?同步機(jī)制保證線程間訪問安全
Java中的同步機(jī)制保證線程間的訪問安全,具體操作分同步塊和同步方法兩種,都需借助synchronized關(guān)鍵字完成[10]。同步塊需要在“臨界區(qū)”的前面加上synchronized關(guān)鍵字并為之配備對象鎖,鎖可以是任意對象,把“臨界區(qū)”內(nèi)容放在對象鎖可控范圍內(nèi);同步方法需要在一個方法前面加上synchronized,表示方法是同步方法,將“臨界區(qū)”放入該方法中執(zhí)行[11]。為保證線程間訪問安全,同步方法包括3個步驟:首先在一個普通方法前面加上synchronized修飾符,變成同步方法,將“臨界區(qū)”的信息放入同步方法中,最后用線程入口run方法調(diào)用同步方法,即可獲得正確結(jié)果。同步塊和同步方法的應(yīng)用保證線程訪問安全的源代碼段如圖5、圖6矩形框和橢圓形框標(biāo)注所示。
同步塊和同步方法都用synchronized修飾符,源程序表述狀態(tài)不同,但執(zhí)行結(jié)果一致。多線程同步原理是用synchronized關(guān)鍵字對“臨界區(qū)”加以保護(hù),保證結(jié)果的正確性,上例中所有票銷售一空,每張票由一個線程所售,票號連續(xù)且按由大到小的順序排列。具體過程如圖7標(biāo)注所示。
圖7?用synchronized鎖確保線程訪問安全
同步機(jī)制的執(zhí)行,是因?yàn)镴ava中引入了“互斥鎖”(監(jiān)視器)?;コ饪梢钥醋魇且环N特殊的同步,同步是一種更為復(fù)雜的互斥[12]。本文重點(diǎn)討論如何借助同步保證線程間的安全,至于同步和互斥的區(qū)別與聯(lián)系在此不進(jìn)行更深入的研究。每個對象都有一個“互斥鎖”標(biāo)志,鎖的作用是保證在任意時刻,只有一個線程訪問該對象,即關(guān)鍵字synchronized與對象的鎖聯(lián)系。當(dāng)某個對象由synchronized修飾時,實(shí)現(xiàn)對臨界資源的互斥操作,被同步synchronized鎖定的代碼段稱為“臨界區(qū)”,每個線程必須獲取到臨界資源所有權(quán)才能執(zhí)行[13]。
同步塊實(shí)施過程(見圖5)包括:當(dāng)線程1進(jìn)入“臨界區(qū)”時,先給obj對象的監(jiān)視器加鎖,執(zhí)行程序后面的代碼,到達(dá)sleep方法時,線程1睡眠了10ms。線程2接著運(yùn)行,到達(dá)同步對象時obj的監(jiān)視器已被加鎖,無法進(jìn)入,JVM將其放入等待區(qū)域中,以此類推線程3,線程4也會被放入等待區(qū)域中。線程1的10ms睡眠狀態(tài)結(jié)束后,繼續(xù)往后執(zhí)行,直到代碼結(jié)束為止,obj對象的監(jiān)視器才被解鎖,由等待區(qū)域的線程2獲得鎖進(jìn)入到同步代碼段中。由此形成了多個線程對同一個對象的“互斥”使用方式,該對象稱為“同步對象”。
圖6的同步方法也需要加鎖,它是給類中的一個this變量的監(jiān)視器加鎖,即給this對象的監(jiān)視器加鎖。當(dāng)線程1進(jìn)入同步方法時,首先查看this對象的監(jiān)視器(this對象的鎖)是否加鎖,加鎖后進(jìn)入到方法內(nèi)部,當(dāng)它睡眠時線程2開始運(yùn)行,因?yàn)閠his對象的監(jiān)視器已加鎖,它只能等待,同樣進(jìn)入等待隊(duì)列的還有線程3、線程4。當(dāng)?shù)谝粋€線程睡醒后執(zhí)行完剩余的代碼返回時,將this對象的監(jiān)視器解鎖,線程2才能進(jìn)入。
2.3?多線程同步塊與同步原理分析
同步分為共享式和分布式兩種,指有多個線程在“臨界區(qū)”上等待消息但互相排斥。Java多線程引用synchronized關(guān)鍵字鎖定“臨界區(qū)”,每個對象都有一個監(jiān)視器(互斥鎖),每個線程首先要獲得監(jiān)視器,才能進(jìn)入synchronized鎖保護(hù)的“臨界區(qū)”,執(zhí)行完“臨界區(qū)”內(nèi)容后釋放監(jiān)視器。期間若某個線程想要獲取的監(jiān)視器被其它線程占用,該線程會被JVM放入等待區(qū)域中,直到監(jiān)視器被占用的線程釋放后,該線程才能進(jìn)入到同步“臨界區(qū)”執(zhí)行代碼段[14]。Java多線程對“臨界區(qū)”的保護(hù)一般以系統(tǒng)通過同步塊或同步方法給對象加鎖的方式實(shí)現(xiàn)。
(1)同步塊實(shí)現(xiàn)線程同步。通過synchronized關(guān)鍵字聲明同步塊。同步塊是一個代碼段,內(nèi)容是“臨界區(qū)”,通過同步塊對“臨界區(qū)”加鎖保證線程執(zhí)行過程的正常秩序,“臨界區(qū)”必須要在線程獲得對象obj的鎖后才能執(zhí)行,對象鎖可以是任意的。執(zhí)行狀態(tài)如下:
synchronized(obj)
{ //“臨界區(qū)”內(nèi)容?}
(2)同步方法實(shí)現(xiàn)線程同步。對一個普通方法用synchronized關(guān)鍵字修飾,即變成同步方法。同步方法的訪問是給類中this變量對象的監(jiān)視器加鎖,同步方法須在獲得調(diào)用該方法類的對象監(jiān)視器后才能執(zhí)行。同步方法一旦執(zhí)行,即獨(dú)占監(jiān)視器,直到從該方法返回時將監(jiān)視器釋放,之前等待的線程才可獲得監(jiān)視器,進(jìn)入可運(yùn)行狀態(tài)。同步機(jī)制確保了同一時刻對于每一個類實(shí)例,其所有聲明為synchronized的成員方法中至多只有一個處于可運(yùn)行狀態(tài),有效解決了多線程間的訪問安全問題[15]。執(zhí)行狀態(tài)如下:
public synchronized void sell()
{?//“臨界區(qū)”內(nèi)容?}
同步塊和同步方法的執(zhí)行過程類似,但同步塊的執(zhí)行靈活性更高。
關(guān)于同步機(jī)制的監(jiān)視器(互斥鎖),還有一種情況需要加以說明,圖6同步方法的this對象監(jiān)視器不適用于同步靜態(tài)方法。在一個類中,有一個靜態(tài)方法訪問了一個靜態(tài)變量,而該靜態(tài)方法又要被多個線程同時訪問,用戶需要對該靜態(tài)方法的訪問進(jìn)行同步。靜態(tài)方法只屬于類本身,不屬于某個對象,調(diào)用靜態(tài)方法時并不需要產(chǎn)生類的對象。因此靜態(tài)方法的訪問同步使用的監(jiān)視器既不是同步方法中this對象的監(jiān)視器,也不是同步塊中任意對象的監(jiān)視器。每個class也有對應(yīng)對象的一把鎖。每個類都對應(yīng)一個class對象,同步靜態(tài)方法使用的是方法所在的類對應(yīng)的Class對象的監(jiān)視器[[16]。
3?同步關(guān)聯(lián)問題
多線程同步時,需要注意兩個問題:同步失效和線程鎖死。
3.1?同步失效
同步的正常執(zhí)行過程是,一個線程獲得該對象的監(jiān)視器后進(jìn)入“臨界區(qū)”,完成所有工作,退出“臨界區(qū)”并釋放監(jiān)視器;下一個線程獲得監(jiān)視器,進(jìn)入“臨界區(qū)”,所有線程共享同一個監(jiān)視器,避免多個線程同時進(jìn)入“臨界區(qū)”,產(chǎn)生訪問沖突。
如果每個線程都有屬于自己獨(dú)占的監(jiān)視器,則每個線程都無須等待另一個線程釋放共享的監(jiān)視器,每個線程才都能進(jìn)入“臨界區(qū)”,該設(shè)想與圖1中源程序不加監(jiān)視器的情況一致,此時線程同步失效。為避免該情況發(fā)生,使所有線程共享同一個監(jiān)視器,確保在任意時刻只有獲得了監(jiān)視器的線程能進(jìn)入“臨界區(qū)”[17]。
run方法是多線程入口方法,任何線程都需要運(yùn)行run( )方法,所以線程run( )方法不能同步,多線程同步同一個對象,每個時間只有一個線程可以執(zhí)行run( )方法,若同步了run( )方法,每個線程必須等待前一個線程運(yùn)行結(jié)束后才開始,此時也會產(chǎn)生同步失效。
3.2?線程死鎖
死鎖是指兩個或者多個線程被永久阻塞的一種局面,產(chǎn)生的前提是有兩個或多個線程操作兩個或多個共同資源[18]。Java多線程同步機(jī)制可能出現(xiàn)兩個線程的情況,兩個線程分別獨(dú)占一個監(jiān)視器,線程1鎖住了對象A的監(jiān)視器,等待對象B的監(jiān)視器,線程2鎖住了對象B的監(jiān)視器,等待對象A的監(jiān)視器。此時每個對象僅憑自己的監(jiān)視器無法完成工作,必須借助另一個對象的監(jiān)視器,因此,每個對象都在等待對方先釋放自己的監(jiān)視器,以此獲得先執(zhí)行的機(jī)會。事實(shí)上,每個對象都不愿釋放自己的監(jiān)視器,先成就對方、自己再獲得執(zhí)行,最終導(dǎo)致了死鎖[19]。
4?結(jié)語
本文以由4個窗口售出100張火車票的過程為例,展示了Java多線程節(jié)省時間、優(yōu)化資源利用率、提高系統(tǒng)執(zhí)行效率等優(yōu)點(diǎn)。但多線程問題繁多、表現(xiàn)復(fù)雜,要保證售票過程中CPU能為4個線程有序地分配資源,確保多個線程共享同一個數(shù)據(jù)時,線程間訪問秩序良好,執(zhí)行結(jié)果正確,需借助Java多線程同步塊或同步方法加鎖的措施保證線程間訪問安全。經(jīng)實(shí)驗(yàn)證明,同步加鎖能解決Java多個線程共享同一個數(shù)據(jù)時潛在的線程安全問題,Java多線程執(zhí)行不僅具有高效性,利用同步機(jī)制還能維護(hù)其穩(wěn)定性和健壯性[20]。
參考文獻(xiàn):
[1]?HYDE P.Java線程編程[M].周良忠,譯.北京:人民郵電出版社,2003.
[2]?OAKS S,WONG H.Java線程[M].第二版.黃若波,等,譯.北京:中國電力出版社,2003.
[3]?結(jié)城浩.JAVA多線程設(shè)計(jì)模式[M].北京:中國鐵道出版社,2005.
[4]?吳紅萍.Java的多線程機(jī)制分析與應(yīng)用[J].軟件導(dǎo)刊,2014(1):114?116.
[5]?回健永.基于Java語言的多線程機(jī)制的實(shí)現(xiàn)[J].天津職業(yè)院校聯(lián)合學(xué)報(bào),2011,13(8):58?61.
[7]?孫超.Java語言中多線程的實(shí)現(xiàn)[J].佳木斯教育學(xué)報(bào),2011(2):428?429.
[8]?張冬姣,孟慶偉,王萍.基于Java多線程的并行計(jì)算技術(shù)研究及應(yīng)用[J].科學(xué)中國人,2014(10):15?16.
[9]?楊軍.多線程在Java中的應(yīng)用及線程同步安全問題的解決方法[J].硅谷,2010(16):153?154.
[10]?李娟.Java多線程同步機(jī)制研究分析[J].中國科教創(chuàng)新導(dǎo)刊,2014(7):183?184.
[11]?路勇.Java多線程同步問題分析[J].軟件,2012,33(4):31?33.
[12]?張步忠.Java語言中的線程同步互斥研究[J].安慶師范學(xué)院學(xué)報(bào):自然科學(xué)版,2011(4):106?110.
[13]?耿祥義,張躍平.Java2實(shí)用教程[M].第5版.北京:清華大學(xué)出版社,2017.
[14]?耿祥義,張躍平.Java2實(shí)用教程[M].第4版.北京:清華大學(xué)出版社,2012.
[15]?張桂珠,張躍平,劉麗.Java面向?qū)ο蟪绦蛟O(shè)計(jì) [M].第3版. 北京:北京郵電大學(xué)出版社.2010.
[16]?葉核亞.Java2程序設(shè)計(jì)實(shí)用教程 [M].第2版.北京:電子工業(yè)出版社,2007.
[17]?ECKEL B.Java編程思想[M].第4版.陳昊鵬,譯.北京:機(jī)械工業(yè)出版社,2007.
[18]?YUYX.Java編程之多線程死鎖與線程間通信簡單實(shí)現(xiàn)代碼[EB/OL].http:∥www.jb51.net/article/126852.htm.
[19]?Java紅茶.Java多線程之死鎖的出現(xiàn)和解決方法[EB/OL].http:∥www.jb51.net/article/126410.htm.
[20]?林炳文.Java多線程學(xué)習(xí)[EB/OL]. https:∥www.cnblogs.com/GarfieldEr007/p/5746362.html.