1. 前言
最近在研究Java进程间通信,为了了解Java中的SharedMemory共享内存。我特地去研究了一些Java NIO进程间通信的方式。
2. Java NIO MappedByteBuffer原理
传统的进程间通信的方式有大致如下几种:
- (1) 管道(PIPE)
- (2) 命名管道(FIFO)
- (3) 信号量(Semphore)
- (4) 消息队列(MessageQueue)
- (5) 共享内存(SharedMemory)
- (6) Socket
Java如何支持进程间通信。我们把Java进程理解为JVM进程。很明显,传统的这些大部分技术是无法被我们的应用程序利用了(这些进程间通信都是靠系统调用来实现的)。但是Java也有很多方法可以进行进程间通信的。 Java nio的MappedByteBuffer也可以通过内存映射文件来实现进程间通信(共享内存)。
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。
3. 实现代码
/**
* locate com.basic
* Created by MasterTj on 2019/3/7.
* "共享内存" 读出数据
*/
public class ReadShareMemory {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
RandomAccessFile raf = new RandomAccessFile("d:/swap.mm", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
int lastIndex = 0;
for(int i=1;i<27;i++){
int flag = mbb.get(0); //取读写数据的标志
int index = mbb.get(1); //读取数据的位置,2 为可读
if(flag != 2 || index == lastIndex){ //假如不可读,或未写入新数据时重复循环
i--;
continue;
}
lastIndex = index;
System.out.println("程序 ReadShareMemory:" + System.currentTimeMillis() +
":位置:" + index +" 读出数据:" + (char)mbb.get(index));
mbb.put(0,(byte)0); //置第一个字节为可读标志为 0
if(index == 27){ //读完数据后退出
break;
}
}
}
}
/**
* locate com.basic
* Created by MasterTj on 2019/3/7.
* 从共享内存中读取数据
*/
public class WriteShareMemory {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
RandomAccessFile raf = new RandomAccessFile("d:/swap.mm", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
//清除文件内容
for(int i=0;i<1024;i++){
mbb.put(i,(byte)0);
}
//从文件的第二个字节开始,依次写入 A-Z 字母,第一个字节指明了当前操作的位置
for(int i=65;i<91;i++){
int index = i-63;
int flag = mbb.get(0); //可读标置第一个字节为 0
if(flag != 0){ //不是可写标示 0,则重复循环,等待
i --;
continue;
}
mbb.put(0,(byte)1); //正在写数据,标志第一个字节为 1
mbb.put(1,(byte)(index)); //写数据的位置
System.out.println("程序 WriteShareMemory:"+System.currentTimeMillis() +
":位置:" + index +" 写入数据:" + (char)i);
mbb.put(index,(byte)i);//index 位置写入数据
mbb.put(0,(byte)2); //置可读数据标志第一个字节为 2
Thread.sleep(513);
}
}
}
代码中使用了读写标志位,和写入的索引位置,所以在 WriteShareMemory 写入一个字符后,只有等待 ReadShareMemory 读出刚写入的字符后才会写入第二个字符。实际应用中可以加入更好的通知方式,如文件锁等。
你也可以查看执行时 c:\swap.mm 文件的内容来验证这一过程,因为 MappedByteBuffer 在运行时是一种 DirectByteBuffer,所以它能与文件即时的同步内容,无须通过 FileChannel 来 write(buffer) 往文件中手工写入数据,或 read(buffer) 手工读数据到内存中。