魏 瑾
(山西機(jī)電職業(yè)技術(shù)學(xué)院,山西 長治 046011)
Java的多線程技術(shù)在實(shí)際應(yīng)用過程中必須解決好各個(gè)線程之間的通信問題,因?yàn)槿狈τ行У耐ㄐ艡C(jī)制,會(huì)造成Java多線程無法合理調(diào)度,進(jìn)而增加系統(tǒng)崩潰的可能性。研究共享內(nèi)存、消息通知等在多線程通信中的原理和應(yīng)用具有重要的意義。
Java在軟件開發(fā)中占據(jù)著非常廣泛的市場,目前是最主流的開發(fā)語言,其優(yōu)點(diǎn)在于執(zhí)行效率高(相對(duì)于Python、C#、PHP等),其中多線程是Java的顯著優(yōu)勢之一。計(jì)算機(jī)中的程序在運(yùn)行時(shí)會(huì)產(chǎn)生一個(gè)獨(dú)立的進(jìn)程,并占據(jù)一定的計(jì)算機(jī)資源,CPU、內(nèi)存等都會(huì)分配給進(jìn)程。在Java語言下,進(jìn)程可存在多個(gè)線程,這些線程并沒有相互獨(dú)立的存儲(chǔ)空間,而是采用共享機(jī)制。多線程的價(jià)值在于能夠分開執(zhí)行不同的任務(wù)或者分段執(zhí)行程序代碼,多線程的應(yīng)用可顯著提高程序的運(yùn)行效率、提高其負(fù)載高并發(fā)的能力以及更加充分的利用計(jì)算機(jī)資源,如CPU的利用率會(huì)因?yàn)槎嗑€程的應(yīng)用而更高。但是從多線程的運(yùn)行情況來看,當(dāng)其在同步執(zhí)行不同的任務(wù)時(shí)難免會(huì)出現(xiàn)資源爭奪的問題,這種情況下就要借助線程間通信來合理地調(diào)度計(jì)算資源,保證每一個(gè)線程的順利執(zhí)行。Java多線程在執(zhí)行層面采用交替進(jìn)行或者接力進(jìn)行,但無論如何實(shí)現(xiàn),都會(huì)用到線程間通信[1]。
在網(wǎng)絡(luò)通信設(shè)計(jì)中必須考慮到效率問題和編碼的難度,網(wǎng)絡(luò)用戶的規(guī)模非常龐大,同一時(shí)間段之內(nèi)可能具有大量的并行的網(wǎng)絡(luò)應(yīng)用,這種情況對(duì)服務(wù)器的負(fù)載能力提出了很高的要求,Java的多線程是提高服務(wù)器利用率以及應(yīng)對(duì)高并發(fā)的重要技術(shù)措施,其應(yīng)用價(jià)值很大,這也是高性能網(wǎng)站大多利用Java來編程的原因之一。
各個(gè)并發(fā)執(zhí)行的線程之間存在爭奪CPU和存儲(chǔ)空間的情況,如果相互之間沒有統(tǒng)一的協(xié)調(diào)機(jī)制,那么程序執(zhí)行必然會(huì)受到嚴(yán)重影響,甚至阻塞,這是開展多線程網(wǎng)絡(luò)通信的主要原因。以下內(nèi)容介紹Java多線程通信的實(shí)現(xiàn)原理。
1.2.1 線程間通信的控制機(jī)制
1) 臨界區(qū)。這種方式主要用于線程訪問共享資源,其特點(diǎn)是速度快,實(shí)現(xiàn)方式為多線程的串行化;2) 互斥量。當(dāng)兩個(gè)線程為互斥的對(duì)象時(shí),其資源空間可共享,其他的線程不能占用其共享空間,這樣可保證其他線程不訪問其公共資源;3) 信號(hào)量。Java的多線程并不能保證同一時(shí)刻所有的線程都能獲得運(yùn)行所需的資源,畢竟CPU和存儲(chǔ)空間的性能是有限的,在確保運(yùn)行效率的基本前提下盡可能多地運(yùn)行線程,這才是比較科學(xué)的策略。信號(hào)量的作用是限制同一時(shí)刻訪問資源的最大線程數(shù)量,顯然,在信號(hào)量的控制下,只有有限數(shù)量線程可運(yùn)行[2];4) 信號(hào)??捎糜谂袛嗑€程的優(yōu)先級(jí),決定了哪些線程先執(zhí)行,哪些線程后執(zhí)行。
1.2.2 線程通信方式
1) 共享內(nèi)存。這是Java多線程通信最典型的方式。其原理是在設(shè)置一個(gè)可由多個(gè)線程共享的變量,計(jì)算機(jī)中的變量實(shí)際上是存儲(chǔ)在內(nèi)存中,當(dāng)程序代碼在執(zhí)行調(diào)用時(shí),會(huì)從內(nèi)存中獲取該共享變量的內(nèi)容,然后由各個(gè)線程共同使用。例如,利用多線程完成頁面抓取任務(wù),為了記錄下一共抓取了過少個(gè)頁面,可設(shè)置一個(gè)共享變量,記為counts,將其初始值設(shè)為0,每抓取一個(gè)頁面,在其基礎(chǔ)上加1,最終的總數(shù)就是抓取頁面的數(shù)量。在多線程模式下,每一個(gè)線程都在單獨(dú)完成頁面抓取的任務(wù),但是計(jì)數(shù)就成為一個(gè)問題,共享變量能被每一個(gè)線程所調(diào)用,這樣就借助該變量實(shí)現(xiàn)了計(jì)數(shù)功能的信息共享,也就完成了通信。在這種模式下,各個(gè)線程之間相互通信的“中間人”就是counts這一變量。其運(yùn)行機(jī)制的示意圖如圖1所示。為了保證共享內(nèi)存的變量具有足夠的安全性和可靠性,可將其存儲(chǔ)在臨界區(qū),并且加鎖或者同步。從這種通信方式的實(shí)現(xiàn)原理來看,各個(gè)線程之間實(shí)際上并沒有實(shí)現(xiàn)直接的通信,而是通過共享變量間接完成通信功能[3]。2) 消息傳遞。這種通信方式是顯性的,各個(gè)線程之間通過傳遞消息來實(shí)現(xiàn)直接的通信,典型的如Actor模型,其中每一個(gè)actor都具有收件和發(fā)件的功能,相互之間借助收發(fā)信息來通信。其收件箱類似于一個(gè)消息隊(duì)列,逐次去執(zhí)行隊(duì)列中的內(nèi)容。消息傳遞的通信方式在應(yīng)對(duì)復(fù)雜業(yè)務(wù)模型時(shí)體現(xiàn)出非常強(qiáng)大的適應(yīng)能力,其應(yīng)用價(jià)值很高。
圖1 Java多線程共享內(nèi)存通信機(jī)制
第一,繼承Thread類并重寫其run()方法。其代碼實(shí)現(xiàn)為public class ThreadName extends Thread{ public void run(){ /*子線程的代碼 */ }}。
第二,利用Runnable接口創(chuàng)建線程類。Java中不能實(shí)現(xiàn)多繼承,在創(chuàng)建線程類時(shí)要先實(shí)現(xiàn)Runnanble接口,并且要重寫該接口中唯一的方法run(),線程的代碼執(zhí)行體就編寫在run()方法中。接口編寫完成之后就可直接繼承該接口的類的對(duì)象。其實(shí)現(xiàn)代碼如下:public class RunnableTest implements Runnable{ public void run(){ /*線程代碼執(zhí)行體*/ }}。
第三,使用Callable和Future創(chuàng)建線程。以上兩種方法雖然都能實(shí)現(xiàn)線程創(chuàng)建的目的,但是在具體應(yīng)用層面卻存在一定的缺陷,最主要的問題是代碼中不能拋出異常,也不能返回值。Callable 接口是Runnable接口的升級(jí)版,其在實(shí)現(xiàn)后者功能的基礎(chǔ)上還具有返回值,該接口依靠call()方法來執(zhí)行線程的代碼。不過這種方式在具體應(yīng)用中還需解決一個(gè)技術(shù)性問題,那就是Callable對(duì)象不能直接作為Thread對(duì)象的target,這時(shí)候就必須引入Future接口[4]。
第一,基于類的調(diào)用方式。無論是繼承Thread類,還是運(yùn)用Runnable接口,調(diào)用線程時(shí)必須先創(chuàng)建出一個(gè)類的對(duì)象,然后再利用該對(duì)象調(diào)用相關(guān)的方法,執(zhí)行線程代碼。Thread類的調(diào)用方式為Threadname test = new ThreadName(),使用Runnable接口創(chuàng)建的類調(diào)用線程,其方式為Threadname test = new RunableTest threadname();Thread RunanbleTest = new thread(test),最終創(chuàng)建的線程為thread RunableTest.start()。
第二,其他調(diào)用方式。1) wait()方法的作用是立即停止當(dāng)前線程,使其處于等待的狀態(tài),與此同時(shí),使用wait()方法處理的線程將會(huì)被置入鎖對(duì)象的等待隊(duì)列中。該線程是否再次執(zhí)行,取決于notify。這一方法使用場景為具有同步作用的代碼塊。2) notify()方法,其作用是喚醒處于等待狀態(tài)下的線程,其使用的位置也是必須在具有同步作用的代碼塊中。3) notifyAll()方法的作用也是喚醒處在等待狀態(tài)下的線程,但其特點(diǎn)是具有普遍性,所有線程均適用。4) join()方法。這一方法在代碼中調(diào)用之后,線程就會(huì)形成一個(gè)隊(duì)列的機(jī)制,當(dāng)前正在執(zhí)行的線程會(huì)暫時(shí)阻塞,join()所屬的線程會(huì)立即執(zhí)行其run()方法,等到其所屬線程執(zhí)行完畢之后,再去執(zhí)行之前受到阻塞的線程的后續(xù)代碼功能。5) 管道。Java中的線程通信經(jīng)常采用管道流的方式,在具體應(yīng)用時(shí)要?jiǎng)?chuàng)建管道的輸出流和輸入流,實(shí)現(xiàn)方式分別為PipedOutputStream pos 和 PipedInputStream pis 。將pos賦給信息輸入線程,而pis賦給信息獲取線程,線程間的通信就此實(shí)現(xiàn)。
第三,優(yōu)化多線程通信的措施。在Java中,服務(wù)器和客戶端之間的通信非常依賴于Socket對(duì)象,該對(duì)象生成之后可用于完成請(qǐng)求應(yīng)答。如果用戶發(fā)起另一個(gè)請(qǐng)求,之前已經(jīng)完成工作的Socket對(duì)象就需要重新連接一次,這樣會(huì)嚴(yán)重消耗服務(wù)器資源。在編程層面可利用ReadMessageThread類來實(shí)現(xiàn)多客戶端的同步管理,系統(tǒng)的安全性會(huì)大幅提升,資源利用率也會(huì)更高。另外,sercerSocket.accept()方法在服務(wù)器端口執(zhí)行一次,這就導(dǎo)致服務(wù)器和客戶端之間的通信只能實(shí)現(xiàn)一次,解決這一問題的方法是讓服務(wù)器和客戶端之間的Socket對(duì)象形成虛擬連接。
Java多線程技術(shù)是其優(yōu)異性能的重要體現(xiàn),在多線程技術(shù)的支持下,使用Java語言制作的網(wǎng)絡(luò)系統(tǒng)大多具有運(yùn)行效率高、高并發(fā)負(fù)載能力強(qiáng)的優(yōu)點(diǎn)。但多線程技術(shù)在實(shí)際應(yīng)用過程中必須重視通信設(shè)計(jì)。在技術(shù)層面主要依靠消息傳遞、共享內(nèi)存等原理。