Java 进程通信(共享内存)



0 写在前面

说到进程通信,我们很轻易就能想到经典的 Socket 通信。但像 Socket 这样的网络通信会增加额外的网络负担,同时也增加了一定的代码量。

共享内存方式是实现进程通信的另外一种方式,其具有数据共享,系统快速查询、动态配置、减少资源耗费等优点。

共享内存特点:

  • 可被多个进程打开访问;
  • 读写操作的进程在执行读写操作时,其他进程不能进行写操作;
  • 多个进程可以交替对某一共享内存执行写操作;
  • 一个进程执行内存写操作后,不影响其他进程对该内存的访问,同时其他进程对更新后的内存具有可见性;

Java 进程间的共享内存通过内存映射文件 NIO (MappedByteBuffer)实现,不同进程的内存映射文件关联到同一物理文件。该文件通常为随机存取文件对象,实现文件和内存的映射,即时双向同步。


1 要点

1.1 MappedByteBuffer

Java IO 操作的 BufferedReader 、 BufferedInputStream 等相信大家都很熟悉,不过在 Java NIO 中引入了一种基于 MappedByteBuffer 操作大文件的方式,其读写性能极高。

MappedByteBuffer 为共享内存缓冲区,实际上是一个磁盘文件的内存映射,实现内存与文件的同步变化,可有效地保证共享内存的实现。

1.2 FileChannel

FileChannel 是将共享内存和磁盘文件建立联系的文件通道类。FileChannel 类的加入是 JDK 为了统一对外设备(文件、网络接口等)的访问方法,并加强了多线程对同一文件进行存取的安全性。我们在这里用它来建立共享内存和磁盘文件间的一个通道。

1.3 RandomAccessFile

RandomAccessFile 是 Java IO 体系中功能最丰富的文件内容访问类,它提供很多方法来操作文件,包括读写支持,与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序可以直接跳到任意地方来读写数据。

举个栗子:

如果我们要向已存在的大小为 1G 的 txt 文本里末尾追加一行文字,内容如下“ Lucene 是一款非常优秀的全文检索库”。其实直接使用 Java 中的流读取 txt 文本里所有的数据转成字符串后,然后拼接“ Lucene 是一款非常优秀的全文检索库”,又写回文本即可。

但如果需求改了,我们要想向大小为 5G 的 txt 文本里追加数据。如果我们电脑的内存只有 4G ,强制读取所有的数据并追加,将会报内存溢出的异常。显然,上面的方法不再合适。

如果我们使用 JAVA IO 体系中的 RandomAccessFile 类来完成的话,可以实现零内存追加。其实,这就是支持任意位置读写类的强大之处。


2 Java进程共享内存实例

(1)写进程

/* NIOWrite.java */

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

public class NIOWrite {

    private static RandomAccessFile raf;
    public static void main(String[] args) throws Exception {
        //建立文件和内存的映射,即时双向同步
        raf = new RandomAccessFile("D:/tmp/data.dat", "rw");
        FileChannel fc = raf.getChannel();
        MappedByteBuffer mbb = fc.map(MapMode.READ_WRITE, 0, 1024);

        //清除文件内容 ,对 MappedByteBuffer 的操作就是对文件的操作
        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(System.currentTimeMillis() +  ":position:" + index +"write:" + (char)i);

            mbb.put(index,(byte)i);     //index 位置写入数据
            mbb.put(0,(byte)2);         //置可读数据标志第一个字节为 2

            Thread.sleep(3000);
        }
    }
}

(2)读进程

/* NIORead.java */

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

public class NIORead {
    private static RandomAccessFile raf;

    public static void main(String[] args) throws Exception {

        raf = new RandomAccessFile("D:/tmp/data.dat", "rw");
        FileChannel fc = raf.getChannel();
        MappedByteBuffer mbb = fc.map(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( System.currentTimeMillis() +  ":position:" + index +"read:" + (char)mbb.get(index));

            mbb.put(0,(byte)0);     //置第一个字节为可读标志为 0    

            if(index == 27){        //读完数据后退出    
                break;
            }
        }
    }
}  

(3)分别运行写进程、读进程(开两个客户端)

运行结果:写进程写入一个字符,读进程才能读一个字符。


3 Java进程共享内存实例(文件锁)

前面方案只有写进程写入一个字符,读进程才能读取一个字符。现在,我们通过文件锁来保证数据读写安全。

(1)写进程

/* NIOWriteLock.java */

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.channels.FileLock;

public class NIOWriteLock {
    private static RandomAccessFile raf;
    public static void main(String[] args) throws Exception {
        //获取随机存取文件对象,建立文件和内存的映射,即时双向同步
        raf = new RandomAccessFile("D:/tmp/data.dat", "rw");
        FileChannel fc = raf.getChannel();      //获取文件通道
        MappedByteBuffer mbb = fc.map(MapMode.READ_WRITE, 0, 1024);  //获取共享内存缓冲区
        FileLock flock=null; 

        for(int i=65;i<91;i++){
            //阻塞独占锁,当文件锁不可用时,当前进程会被挂起      
            flock=fc.lock();
            System.out.println(System.currentTimeMillis() +  ":write:" + (char)i);
            mbb.put(i-65,(byte)i);  //从文件第一个字节位置开始写入数据    
            flock.release();        //释放锁  
            Thread.sleep(1000);
        }

    }
}  

(2)读进程

/* NIOReadLock.java */

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.FileChannel.MapMode;

public class NIOReadLock {
    private static RandomAccessFile raf;

    public static void main(String[] args) throws Exception {

        raf = new RandomAccessFile("D:/tmp/data.dat", "rw");
        FileChannel fc = raf.getChannel();
        MappedByteBuffer mbb = fc.map(MapMode.READ_WRITE, 0, 1024);
        FileLock flock=null;

        for(int i=0;i<26;i++){
            flock=fc.lock();    //上锁  
            System.out.println( System.currentTimeMillis() +  ":read:" + (char)mbb.get(i));
            flock.release();    //释放锁  
            Thread.sleep(1000);
        }
    }
}  

(3)分别运行写进程、读进程

进程不再是写完一个字符才能读取一个字符,因为我们采用了文件锁方式来规范读写操作。

该方法在读操作和写操作之前都采用加锁来保证数据安全。


参考资料:


 
comments powered by Disqus