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