1 共用記憶體對應應用開發的意義
對熟知UNIX系統應用開發的程式員來說,IPC(InterProcess Communication)機制是非常熟悉的,IPC基本包括共用記憶體、號誌操作、訊息佇列、訊號處理等部分,是開發應用中非常重要的必不可少的工具。其中共用記憶體IPC機制的關鍵,對於資料共用、系統快速查詢、動態配置、減少資源耗費等均有獨到的優點。
對應UNIX系統來說,共用記憶體分為一般共用記憶體和映像檔案分享權限設定記憶體兩種,而對應 Windows,實際上只有映像檔案分享權限設定記憶體一種。所以java應用中也是只能建立映像檔案分享權限設定記憶體。
在java語言中,基本上沒有提及共用記憶體這個概念,但是,在某一些應用中,共用記憶體確實非常有用,例如採用java語言的分布式應用系統中,存在著大量的分布式共用對象,很多時候需要查詢這些對象的狀態,以查看系統是否運行正常或者瞭解這些對象的目前的一些統計資料和狀態。如果採用網路通訊的方式,顯然會增加應用的額外負擔,也增加了一些不必要的應用編程。而如果採用共用記憶體的方式,則可以直接通過共用記憶體查看對象的狀態資料和統計資料,從而減少了一些不必要的麻煩。
共用記憶體的使用有如下幾個特點:
可以被多個進程開啟訪問;
讀寫操作的進程在執行讀寫操作時其他進程不能進行寫操作;
多個進程可以交替對某一共用記憶體執行寫操作;
一個進程執行了記憶體的寫操作後,不影響其他進程對該記憶體的訪問。同時其他進程對更新後的記憶體具有可見度。
在進程執行寫操作時如果異常退出,對其他進程寫操作禁止應自動解除。
相對共用檔案,資料訪問的方便性和效率有
另外,共用記憶體的使用上有如下情況:
獨佔的寫操作,相應有獨佔的寫操作等待隊列。獨佔的寫操作本身不會發生資料的一致性問題。
共用的寫操作,相應有共用的寫操作等待隊列。共用的寫操作則要注意防止發生資料的一致性問題。
獨佔的讀操作,相應有共用的讀操作等待隊列;
共用的讀操作,相應有共用的讀操作等待隊列。
一般情況下,我們只是關心第一二種情況。
2 共用記憶體在java中的實現
在jdk1.4中提供的類MappedByteBuffer為我們實現共用記憶體提供了較好的方法。該緩衝區實際上是一個磁碟檔案的記憶體映像。二者的變化將保持同步,即記憶體資料發生變化會立刻反映到磁碟檔案中,這樣會有效保證共用記憶體的實現。
將共用記憶體和磁碟檔案建立聯絡的是檔案通道類:FileChannel。該類的加入是JDK為了統一對外部裝置(檔案、網路介面等)的存取方法,並且加強了多線程對同一檔案進行存取的安全性。例如讀寫操作統一成read和write。這裡只是用它來建立共用記憶體用,它建立了共用記憶體和磁碟檔案之間的一個通道。
開啟一個檔案建立一個檔案通道可以用RandomAccessFile類中的方法getChannel。該方法將直接返回一個檔案通道。該檔案通道由於對應的檔案設為隨機存取檔案,一方面可以進行讀寫兩種操作,另一方面使用它不會破壞映像檔案的內容(如果用FileOutputStream直接開啟一個映像檔案會將該檔案的大小置為0,當然資料會全部丟失)。這裡,如果用 FileOutputStream和FileInputStream則不能理想的實現共用記憶體的要求,因為這兩個類同時實現自由的讀寫操作要困難得多。
下面的代碼實現了如上功能,它的作用類似UNIX系統中的mmap函數。
// 獲得一個唯讀隨機存取檔案對象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 獲得相應的檔案通道
FileChannel fc = RAFile.getChannel();
// 取得檔案的實際大小,以便映像到共用記憶體
int size = (int)fc.size();
// 獲得共用記憶體緩衝區,該共用記憶體唯讀
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);
// 獲得一個可讀寫的隨機存取檔案對象
RAFile = new RandomAccessFile(filename,"rw");
// 獲得相應的檔案通道
fc = RAFile.getChannel();
// 取得檔案的實際大小,以便映像到共用記憶體
size = (int)fc.size();
// 獲得共用記憶體緩衝區,該共用記憶體可讀寫
mapBuf = fc.map(FileChannel.MAP_RW,0,size);
// 擷取頭部訊息:存取許可權
mode = mapBuf.getInt();
如果多個應用映像同一檔案名稱的共用記憶體,則意味著這多個應用共用了同一記憶體資料。這些應用對於檔案可以具有同等存取許可權,一個應用對資料的重新整理會更新到多個應用中。
為了防止多個應用同時對共用記憶體進行寫操作,可以在該共用記憶體的頭部資訊加入寫操作標誌。該共用記憶體的頭部基本資料至少有:
int Length; // 共用記憶體的長度。
int mode; // 該共用記憶體目前的存模數式。
共用記憶體的頭部資訊是類的私人資訊,在多個應用可以對同一共用記憶體執行寫操作時,開始執行寫操作和結束寫操作時,需調用如下方法:
public boolean StartWrite()
{
if(mode == 0) { // 標誌為0,則表示可寫
mode = 1; // 置標誌為1,意味著別的應用不可寫該共用記憶體
mapBuf.flip();
mapBuf.putInt(mode); // 寫如共用記憶體的頭部資訊
return true;
}
else {
return false; // 指明已經有應用在寫該共用記憶體,本應用不可寫該共用記憶體
}
}
public boolean StopWrite()
{
mode = 0; // 釋放寫入權限
mapBuf.flip();
mapBuf.putInt(mode); // 寫入共用記憶體頭部資訊
return true;
}
這裡提供的類檔案mmap.java封裝了共用記憶體的基本介面,讀者可以用該類擴充成自己需要的功能全面的類。
如果執行寫操作的應用異常中止,那麼映像檔案的共用記憶體將不再能執行寫操作。為了在應用異常中止後,寫操作禁止標誌自動消除,必須讓啟動並執行應用獲知退出的應用。在多線程應用中,可以用同步方法獲得這樣的效果,但是在多進程中,同步是不起作用的。方法可以採用的多種技巧,這裡只是描述一可能的實現:採用檔案鎖的方式。寫共用記憶體應用在獲得對一個共用記憶體寫入權限的時候,除了判斷頭部資訊的寫入權限標誌外,還要判斷一個臨時的鎖檔案是否可以得到,如果可以得到,則即使頭部資訊的寫入權限標誌為1(上述),也可以啟動寫入權限,其實這已經表明寫入權限獲得的應用已經異常退出,這段代碼如下:
// 開啟一個臨時的檔案,注意同一共用記憶體,該檔案名稱要相同,可以在共用檔案名稱後加尾碼“.lock”。
RandomAccessFile fis = new RandomAccessFile("shm.lock","rw");
// 獲得檔案通道
FileChannel lockfc = fis.getChannel();
// 獲得檔案的獨佔鎖,該方法不產生堵塞,立刻返回
FileLock flock = lockfc.tryLock();
// 如果為空白,則表明已經有應用佔有該鎖
if(flock == null) {
...// 不能執行寫操作
}
else {
...// 可以執行寫操作
}
該鎖會在應用異常退出後自動釋放,這正是該處所需要的方法。
3 共用記憶體在java中的應用
共用記憶體在java應用中,經常有如下兩種種應用:
永久對象配置。
在java伺服器應用中,使用者可能會在運行過程中配置一些參數,而這些參數需要永久有效,當伺服器應用重新啟動後,這些配置參數仍然可以對應用起作用。這就可以用到該文中的共用記憶體。該共用記憶體中儲存了伺服器的運行參數和一些對象運行特性。可以在應用啟動時讀入以啟用以前配置的參數。
查詢共用資料。
一個應用(例 sys.java)是系統的服務進程,其系統的運行狀態記錄在共用記憶體中,其中運行狀態可能是不斷變化的。為了隨時瞭解系統的運行狀態,啟動另一個應用(例 mon.java),該應用查詢該共用記憶體,彙報系統的運行狀態。
可見,共用記憶體在java應用中還是很有用的,只要組織好共用記憶體的資料結構,共用記憶體就可以在應用開發中發揮很不錯的作用。