戈 俊
現(xiàn)實生活中,體育賽事的售票一般是并發(fā)執(zhí)行,要求多窗口同時進行售票任務(wù)。并且要保證售票順利進行,不可以出現(xiàn)錯票現(xiàn)象。如何通過多線程同步技術(shù)解決錯票問題,是本研究的主要目的。
根據(jù)研究內(nèi)容和研究目的,查閱了近年來有關(guān)多線程技術(shù)等方面的專著、期刊、論文和資料,并對資料進行整理分析、篩選、歸納、概括。為寫作提供依據(jù),為后續(xù)研究提供了充足的理論支持。
通過Eclipse集成開發(fā)軟件,建立JavaSE開源項目,通過創(chuàng)建包、接口、類、配置文件等方法,進行項目開發(fā)的基本配置,通過WindowBuilder插件,進行GUI可視化組件開發(fā),使用多線程技術(shù)開發(fā)體育賽事售票系統(tǒng),結(jié)合錯票問題提出解決方案。
3.1.1 進程與線程的關(guān)系分析
進程顧名思義是正在進行中的程序。當(dāng)我們在執(zhí)行一個程序時,程序啟動后會在內(nèi)存中開辟空間,這個被開辟的空間就是進程,進程是一個應(yīng)用程序?qū)?yīng)內(nèi)存中的一片空間,等待程序運行完畢后,會將此片空間釋放掉,硬盤是持久化存儲,而內(nèi)存是程序運行時臨時存儲的。線程是任意進程在內(nèi)存中的執(zhí)行路徑,當(dāng)進程開辟多個執(zhí)行路徑并同時操作多部分代碼時,即開啟多個線程任務(wù)。
3.1.2 多線程創(chuàng)建方式分析
創(chuàng)建線程目的是為了開啟一條執(zhí)行路徑去運行指定的代碼,和其他代碼實現(xiàn)同時運行,而運行的指定代碼就是這個執(zhí)行路徑的任務(wù)[1]。Java中Thread類用于描述線程,線程是需要任務(wù),這個任務(wù)就通過Thread類中的run方法來體現(xiàn),run方法就是封裝自定義線程運行任務(wù)的函數(shù)[1]。run方法中定義的就是線程要運行的任務(wù)代碼,開啟線程是為了運行指定代碼,所以只有繼承Thread類并復(fù)寫run方法,將運行的代碼定義在run方法中即可[1]。
Oracle公司在定義Thread類時,先定義了一個私有的Runnable接口引用的全局變量[2]。定義一個帶參構(gòu)造函數(shù),而構(gòu)造函數(shù)的參數(shù)就是Runnable接口。通過參數(shù)傳遞,將局部變量的參數(shù)傳遞給全局變量。同時在Thread類的run方法中定義了如果全局變量不為空的話。就運行實現(xiàn)Runnable接口子類對象中的run方法[2]。Runnable接口的出現(xiàn),僅僅是將線程任務(wù)進行了對象的封裝。
3.1.3 多線程運行時內(nèi)存管理分析
只要開啟一條執(zhí)行路徑,棧內(nèi)存中就隨即存在了一條單獨的執(zhí)行路徑,當(dāng)程序運行時調(diào)用到了主方法,主線程即可被創(chuàng)建出來,主線程在順序執(zhí)行的過程中,會執(zhí)行到繼承自線程Thread對象的子類對象,通過關(guān)鍵字new線程對象時,即創(chuàng)建了新的線程。當(dāng)主線程讀到start方法時,在棧內(nèi)存中開啟了新的線程路徑,在主線程中執(zhí)行的內(nèi)容是main方法中的內(nèi)容,而新的線程路徑中執(zhí)行的則是繼承自Thread線程對象被子類覆蓋的run方法中的內(nèi)容。每條線程在棧內(nèi)存中都被分配了獨立的空間。同時,主線程中調(diào)用的方法就在主線程中壓棧彈棧,而run方法中調(diào)用的方法將在新開辟的路徑中壓棧彈棧,新開啟的線程都是由Thread-加上數(shù)字來命名的。也就是說每個run方法中都有自己所屬的棧區(qū),run方法中定義的局部變量也都在各自的棧區(qū)run方法內(nèi)[3]。一旦run方法內(nèi)的所有任務(wù)執(zhí)行完,該線程的run方法彈棧,該線程所分配的執(zhí)行空間被釋放。多線程程序運行時,即便主函數(shù)先運行完畢彈棧后,該程序的其他正在運行的線程依然存在,保證著程序的正常運行。
3.1.4 多線程執(zhí)行的狀態(tài)分析
當(dāng)應(yīng)用程序在執(zhí)行時,CPU在多個執(zhí)行線程中做著高速切換,這個切換是隨機的。一旦線程處于運行狀態(tài)時,CPU在對其進行處理將分兩種狀態(tài):正在被處理表明該線程具備CPU的執(zhí)行資格和執(zhí)行權(quán);在處理隊列中排隊表明該線程具備著CPU的執(zhí)行資格[4]。當(dāng)線程運行時執(zhí)行到sleep方法或wait方法時就進入凍結(jié)狀態(tài),這是線程在釋放執(zhí)行權(quán)的同時釋放了執(zhí)行資格的過程。如果想讓凍結(jié)的線程恢復(fù)到運行狀態(tài),可以等待設(shè)置的休眠時間times up或使用notify喚醒線程。當(dāng)被凍結(jié)的線程被喚醒后將進入兩種狀態(tài)(運行狀態(tài)/臨時阻塞狀態(tài)),處于臨時阻塞狀態(tài)的線程具備著執(zhí)行資格,但是不具備執(zhí)行權(quán)[5]。此時就看CPU有沒有切到該線程上。當(dāng)一個線程被創(chuàng)建后,通過start方法開啟并運行該線程,如果線程任務(wù)結(jié)束后那就是消亡狀態(tài),通過stop方法也可以結(jié)束線程使線程進入消亡狀態(tài)。
3.2.1 建立售票對象時繼承Thread出現(xiàn)嚴重錯票現(xiàn)象
首先創(chuàng)建票的數(shù)量作為該類的全局變量,此時把票定義為100張。通過循環(huán)語句,可以執(zhí)行售票方法。票務(wù)將進行遞減操作,售完一張在票務(wù)的總數(shù)上遞減一張。由于門票對象已經(jīng)繼承了線程對象,那么該門票就是線程對象,可以創(chuàng)建多個售票窗口的同時調(diào)用線程類中的run方法執(zhí)行售票程序。此時系統(tǒng)將出現(xiàn)一個問題,4個線程分別都售出了100張門票,共計售出400張門票,出現(xiàn)了嚴重的錯票現(xiàn)象。主要原因是每個對象創(chuàng)建,堆內(nèi)存中都有個引用變量門票數(shù)ticketNumber,默認初始化為0,顯示初始化為100。但在調(diào)用過程中,每個線程中間都有自己的run方法,而執(zhí)行的時候每個run都有自己的對象所屬。所以引用變量ticketNumber在各自對象中進行操作,于是4個線程操作了4個ticketNumber。
3.2.2 通過實現(xiàn)Runnable接口臨時解決錯票問題
出現(xiàn)錯票現(xiàn)象以后,可以換一種思路,通過實現(xiàn)Runnable接口進行多線程的售票。這樣將符合實現(xiàn)Runnable接口的優(yōu)勢,將線程任務(wù)進行獨立的封裝[2]。同時開啟4個線程將售票任務(wù)作為參數(shù)傳遞給4個線程,4個線程將共同操作同一個任務(wù),這樣就不會出現(xiàn)錯票現(xiàn)象。
很明顯,4個線程同時在售賣100張票,此時并沒有出現(xiàn)錯票現(xiàn)象。但是這種情況真的不會出現(xiàn)錯票現(xiàn)象嗎?如果4個線程共同售賣同100張門票時,其中有一個線程臨時處于了等待狀態(tài)并沒有及時售出門票。當(dāng)該線程處于等待狀態(tài)時,線程任務(wù)的執(zhí)行權(quán)將被切換到其他線程身上。再次將執(zhí)行權(quán)切回到本線程身上時,就有可能出現(xiàn)錯票現(xiàn)象。這種現(xiàn)象,很容易出現(xiàn)在最后幾張票的售賣過程中,往往會出現(xiàn)負數(shù)票現(xiàn)象。我們可以通過讓線程休眠若干毫秒來模擬實現(xiàn)錯票現(xiàn)象。如圖1所示。
圖1 讓線程短暫休眠后的錯票現(xiàn)象
3.2.3 實現(xiàn)Runnable接口后錯票現(xiàn)象依然存在的原因分析
通過讓線程休眠若干毫秒來模擬線程安全問題時,可以假設(shè)當(dāng)門票已經(jīng)售賣到最后一張,那么門票數(shù)ticketNumber就為1。而4個線程在爭奪執(zhí)行權(quán)的同時都進入了售票循環(huán)系統(tǒng)中,判斷條件是ticketNumber只要大于0就可以繼續(xù)進入If條件語句的執(zhí)行體內(nèi),ticketNumber目前等于1已經(jīng)滿足If判斷語句的條件,任何一個線程進入判斷語句的執(zhí)行體內(nèi),由于先執(zhí)行到之前為了模擬票務(wù)系統(tǒng)出現(xiàn)錯誤的休眠語句,讓該線程在if執(zhí)行語句的執(zhí)行體內(nèi)休眠1000毫秒。與此同時,if執(zhí)行體內(nèi)的執(zhí)行語句并未被執(zhí)行,于是門票數(shù)ticketNumber并未得到改變。而當(dāng)該線程處于休眠狀態(tài)以后,釋放了執(zhí)行權(quán)。其他線程獲取了執(zhí)行權(quán)以后,將直接進入if語句的條件判斷,由于ticketNumber仍然是大于0,所以該線程仍然可以進入if的執(zhí)行體內(nèi)。以此類推,每個線程進入if判斷語句的執(zhí)行體內(nèi),都會按照我們事先設(shè)定好的讓線程休眠1000毫秒,讓線程釋放執(zhí)行權(quán)。而當(dāng)每個線程依次恢復(fù)執(zhí)行狀態(tài)時,都會進行ticketNumber減減的動作,這時就會出現(xiàn)錯票現(xiàn)象。出現(xiàn)負票,因為ticketNumber分別被4個線程從1減到0,從0減到-1,從-1減到-2。
3.2.4 通過同步代碼塊或同步函數(shù)解決錯票問題
當(dāng)一個線程讀到synchronized同步代碼塊時,該線程會判斷并檢查同步代碼塊中的參數(shù)鎖對象是否存在[6]。如果存在將攜帶該鎖進入同步內(nèi)繼續(xù)執(zhí)行;如果不存在即便已獲取執(zhí)行權(quán)的線程也無法進入同步內(nèi),因為無法獲取并持有同步鎖對象。持有同步代碼塊參數(shù)對象的線程,即使在同步內(nèi)休眠釋放執(zhí)行權(quán),其他線程也無法持有對象鎖,將無法進入同步內(nèi)執(zhí)行相應(yīng)代碼,只有當(dāng)該線程執(zhí)行完同步內(nèi)所有代碼,出同步代碼塊時,該線程會釋放同步鎖對象。這樣其他線程才有可能持有該同步鎖進入同步內(nèi),這樣就避免了出現(xiàn)線程安全的問題。如圖2所示。
圖2 同步代碼塊將操作共享數(shù)據(jù)的多條代碼進行封裝解決錯票問題
體育賽事的門票售賣多為并發(fā)執(zhí)行,要求多窗口同時進行售票任務(wù),在技術(shù)選型上傾向于多線程技術(shù),由于線程任務(wù)執(zhí)行時會偶發(fā)臨時阻塞狀態(tài),待運行狀態(tài)恢復(fù)時極易觸發(fā)錯票事故,主要問題在于一個線程在操作多條作為共享數(shù)據(jù)的體育賽事門票代碼的同時,其他線程也有可能在爭奪執(zhí)行權(quán)的情況下參與運算。
將作為共享數(shù)據(jù)的體育賽事門票代碼封裝打包成一個整體并加上鎖,當(dāng)一個線程拿到鎖進入封裝體內(nèi)售賣門票時,其他線程無法獲取該封裝體的鎖,于是無法參與同時售賣,此舉有效地避免了錯票事故的發(fā)生。只有當(dāng)售票線程結(jié)束售票任務(wù)離開封裝體后,將鎖移交其他線程,這樣其他線程才有可能效仿前者執(zhí)行售票任務(wù)。