Java NIO 应用 -- 使用内存映射文件实现进程间通信

一看到 Java NIO 的内存映射文件(MappedByteBuffer),让我立即就联想到 Windows 系统的内存映射文件。Windows 系统的内存映射文件能用来在多个进程间共享数据,即进程间的共享内存,是通过把同一块内存区域映射到不同进程的地址空间中,从而达到共享内存。

Java NIO 的内存映射文件和 Windows 系统下的一样,都能把物理文件的内容映射到内存中,那么 MappedByteBuffer 是否能用来在不同 Java 进程(JVM) 间共享数据呢?答案是肯定的,这样在通常的 Socket 方式来实现 Java 进程间通信之上又多了一种方法。

在 Windows 中内存映射文件可以是脱离物理文件而存在的一块命名的内存区域,使用相同的内存映射名就能在不同的进程中共享同一片内存。然后,Java 的 MappedByteBuffer 总是与某个物理文件相关的,因为不管你是从 FileInputStream、FileOutputStream 还是 RandomAccessFile 得来的 FileChannel,再 map() 得到的内存映射文件 MappedByteBuffer,如果在构造 FileInputStream、FileOutputStream、RandomAccessFile 对象时不指定物理文件便会有 FileNotFoundException 异常。

所以 Java NIO 来实现共享内存的办法就是让不同进程的内存映射文件关联到同一个物理文件,因为 MappedByteBuffer 能让内存与文件即时的同步内容。严格说来,称之为内存共享是不准确的,其实就是两个 Java 进程通过中间文件来交换数据,用中间文件使得两个进程的两块内存区域的内容得到及时的同步。

用图来理解 Java NIO 的“共享内存”的实现原理:

知道了实现原理之后,下面用代码来演示两个进程间用内存映射文件来进行数据通信。代码 WriteShareMemory.java 往映射文件中依次写入 A、B、C ... Z,ReadShareMemory.java 逐个读出来,打印到屏幕上。代码对交换文件 swap.mm 的第一个字节作了读写标志,分别是 0-可读,1-正在写,2-可读。RandomAccessFile 得到的 Channel 能够灵活的进行读或写,并且不会破坏原有文件内容,而 FileInputStream 或 FileOutputStream 取得的 Channel 则很难达到这一功效,所以使用了 RandomAccessFile 来获得 FileChannel。

WriteShareMemory.java
 1package com.unmi;
 2
 3import java.io.RandomAccessFile;
 4import java.nio.MappedByteBuffer;
 5import java.nio.channels.FileChannel;
 6import java.nio.channels.FileChannel.MapMode;
 7
 8/**
 9 * 往 "共享内存" 写入数据
10 * @author Unmi
11 */
12public class WriteShareMemory {
13
14    /**
15     * @param args
16     * @throws Exception
17     */
18    public static void main(String[] args) throws Exception {
19        RandomAccessFile raf = new RandomAccessFile("c:/swap.mm", "rw");
20        FileChannel fc = raf.getChannel();
21        MappedByteBuffer mbb = fc.map(MapMode.READ_WRITE, 0, 1024);
22
23        //清除文件内容
24        for(int i=0;i<1024;i++){
25            mbb.put(i,(byte)0);
26        }
27
28        //从文件的第二个字节开始,依次写入 A-Z 字母,第一个字节指明了当前操作的位置
29        for(int i=65;i<91;i++){
30            int index = i-63;
31            int flag = mbb.get(0); //可读标置第一个字节为 0
32            if(flag != 0){ //不是可写标示 0,则重复循环,等待
33                i --;
34                continue;
35            }
36            mbb.put(0,(byte)1); //正在写数据,标志第一个字节为 1
37            mbb.put(1,(byte)(index)); //写数据的位置
38
39            System.out.println("程序 WriteShareMemory:"+System.currentTimeMillis() +
40                    ":位置:" + index +" 写入数据:" + (char)i);
41
42            mbb.put(index,(byte)i);//index 位置写入数据
43            mbb.put(0,(byte)2); //置可读数据标志第一个字节为 2
44            Thread.sleep(513);
45        }
46    }
47}

ReadShareMemory.java
 1package com.unmi;
 2
 3import java.io.RandomAccessFile;
 4import java.nio.MappedByteBuffer;
 5import java.nio.channels.FileChannel;
 6import java.nio.channels.FileChannel.MapMode;
 7
 8/**
 9 * 从 "共享内存" 读出数据
10 * @author Unmi
11 */
12public class ReadShareMemory {
13
14    /**
15     * @param args
16     * @throws Exception
17     */
18    public static void main(String[] args) throws Exception {
19        RandomAccessFile raf = new RandomAccessFile("c:/swap.mm", "rw");
20        FileChannel fc = raf.getChannel();
21        MappedByteBuffer mbb = fc.map(MapMode.READ_WRITE, 0, 1024);
22        int lastIndex = 0;
23
24        for(int i=1;i<27;i++){
25            int flag = mbb.get(0); //取读写数据的标志
26            int index = mbb.get(1); //读取数据的位置,2 为可读
27
28            if(flag != 2 || index == lastIndex){ //假如不可读,或未写入新数据时重复循环
29                i--;
30                continue;
31            }
32
33            lastIndex = index;
34            System.out.println("程序 ReadShareMemory:" + System.currentTimeMillis() +
35                    ":位置:" + index +" 读出数据:" + (char)mbb.get(index));
36
37            mbb.put(0,(byte)0); //置第一个字节为可读标志为 0
38
39            if(index == 27){ //读完数据后退出
40                break;
41            }
42        }
43    }
44}

在 Eclipse 中运行 WriteShareMemory,然后到命令行下运行 ReadShareMemory,你将会看到 WriteShareMemory 写一个字符,ReadShareMemory 读一个。


代码中使用了读写标志位,和写入的索引位置,所以在 WriteShareMemory 写入一个字符后,只有等待 ReadShareMemory 读出刚写入的字符后才会写入第二个字符。实际应用中可以加入更好的通知方式,如文件锁等。

你也可以查看执行时 c:\swap.mm 文件的内容来验证这一过程,因为 MappedByteBuffer 在运行时是一种 DirectByteBuffer,所以它能与文件即时的同步内容,无须通过 FileChannel 来 write(buffer) 往文件中手工写入数据,或 read(buffer) 手工读数据到内存中。

参考:1. 共享内存在Java中实现和应用

永久链接 https://yanbin.blog/java-nio-memory-mapping-communicate/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。