一、共享内存

特点:

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

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

1.1 要点

1.1.1 MappedByteBuffer

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

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

1.1.2 FileChannel

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

1.1.3 RandomAccessFile

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

举个栗子:

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

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

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

1.2 Java进程共享内存实例

1.2.1 写进程

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);
        }
    }
}

1.2.2 读进程

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;
            }
        }
    }
}

1.2.3 分别运行两个进程

在idea中先开始运行read进程,没有反应,这时启动write进程,可以看到两个进程开始了同步的读写
write和read的输入分别如下:
write:

1598935551847:position:2write:A
1598935554848:position:3write:B
1598935557848:position:4write:C
1598935560848:position:5write:D
1598935563848:position:6write:E
1598935566849:position:7write:F
1598935569850:position:8write:G
1598935572850:position:9write:H
1598935575851:position:10write:I
1598935578852:position:11write:J
1598935581852:position:12write:K
1598935584853:position:13write:L
1598935587854:position:14write:M
1598935590854:position:15write:N
1598935593854:position:16write:O
1598935596855:position:17write:P
1598935599855:position:18write:Q
1598935602856:position:19write:R
1598935605856:position:20write:S
1598935608857:position:21write:T
1598935611858:position:22write:U
1598935614858:position:23write:V
1598935617858:position:24write:W
1598935620858:position:25write:X
1598935623859:position:26write:Y
1598935626859:position:27write:Z

Process finished with exit code 0

read:

1598935551847:position:2read:A
1598935554848:position:3read:B
1598935557848:position:4read:C
1598935560848:position:5read:D
1598935563848:position:6read:E
1598935566849:position:7read:F
1598935569850:position:8read:G
1598935572850:position:9read:H
1598935575851:position:10read:I
1598935578852:position:11read:J
1598935581852:position:12read:K
1598935584853:position:13read:L
1598935587854:position:14read:M
1598935590854:position:15read:N
1598935593854:position:16read:O
1598935596855:position:17read:P
1598935599855:position:18read:Q
1598935602856:position:19read:R
1598935605856:position:20read:S
1598935608857:position:21read:T
1598935611858:position:22read:U
1598935614858:position:23read:V
1598935617858:position:24read:W
1598935620858:position:25read:X
1598935623859:position:26read:Y
1598935626859:position:27read:Z

Process finished with exit code 0

可以看出两个进程读写时间基本一致

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

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

1.3.1 写进程

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);
        }

    }
}

1.3.2 读进程

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);
        }
    }
}

1.3.3 分别运行两个进程

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

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