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