Java 中 RandomAccessFile 的并发性

我正在创建一个对象,通过多个线程写入文件(在SSD上)。每个线程都尝试在文件中的特定位置写入一个直接字节缓冲区,我确保线程写入的位置不会与另一个线程重叠:RandomAccessFile

file_.getChannel().write(buffer, position);

其中 是 的实例,并且是直接字节缓冲区。file_RandomAccessFilebuffer

对于 RandomAccessFile 对象,由于我没有使用 fallocate 来分配文件,并且文件的长度正在发生变化,这会利用底层媒体的并发性吗?

如果不是,那么在创建文件时使用上述函数而不调用 fallocate 有什么意义吗?


答案 1

我用下面的代码做了一些测试:

   public class App {
    public static CountDownLatch latch;

    public static void main(String[] args) throws InterruptedException, IOException {
        File f = new File("test.txt");
        RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
        latch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(new WritingThread(i, (long) i * 10, file.getChannel()));
            t.start();

        }
        latch.await();
        file.close();
        InputStream fileR = new FileInputStream("test.txt");
        byte[] bytes = IOUtils.toByteArray(fileR);
        for (int i = 0; i < bytes.length; i++) {
            System.out.println(bytes[i]);

        }  
    }

    public static class WritingThread implements Runnable {
        private long startPosition = 0;
        private FileChannel channel;
        private int id;

        public WritingThread(int id, long startPosition, FileChannel channel) {
            super();
            this.startPosition = startPosition;
            this.channel = channel;
            this.id = id;

        }

        private ByteBuffer generateStaticBytes() {
            ByteBuffer buf = ByteBuffer.allocate(10);
            byte[] b = new byte[10];
            for (int i = 0; i < 10; i++) {
                b[i] = (byte) (this.id * 10 + i);

            }
            buf.put(b);
            buf.flip();
            return buf;

        }

        @Override
        public void run() {
            Random r = new Random();
            while (r.nextInt(100) != 50) {
                try {
                    System.out.println("Thread  " + id + " is Writing");
                    this.channel.write(this.generateStaticBytes(), this.startPosition);
                    this.startPosition += 10;
                } catch (IOException e) {
                    e.printStackTrace();

                }
            }
            latch.countDown();
        }
    }
}

到目前为止,我所看到的:

  • Windows 7(NTFS分区):线性运行(即一个线程写入,当它结束时,另一个线程开始运行)

  • Linux Parrot 4.8.15 (ext4 partition) (基于 Debian 的发行版),与 Linux Kernel 4.8.0:线程在执行过程中混合在一起

同样,正如文档所说:

文件通道可由多个并发线程安全使用。可以随时调用 close 方法,如 Channel 接口所指定。在任何给定时间,只有一个涉及通道位置或可以更改其文件大小的操作可能正在进行;在第一个操作仍在进行时尝试启动第二个此类操作将阻塞,直到第一个操作完成。其他行动,特别是那些采取明确立场的行动,可同时进行。它们是否实际这样做取决于基础实现,因此未指定。

因此,我建议首先尝试一下,看看要将代码部署到的操作系统(可能是文件系统类型)是否支持并行执行调用。FileChannel.write

编辑:如前所述,上述并不意味着线程可以并发写入文件,实际上与调用的行为相反,因为调用的行为符合WriteableByteChannel的合约,该合约明确指定一次只有一个线程可以写入给定文件:write

如果一个线程在通道上启动写入操作,则尝试启动另一个写入操作的任何其他线程都将阻塞,直到第一个操作完成


答案 2

正如文档所述,并且 Adonis 已经提到了这一点,一次只能由一个线程执行写入。您不会通过并发获得性能提升,此外,如果这是一个实际问题,则只应担心性能,因为并发写入磁盘实际上可能会降低性能(SSD可能比HDD少)。

在大多数情况下,底层媒体(SSD,HDD,网络)是单线程的 - 实际上,在硬件级别上没有线程这样的东西,线程只不过是一种抽象。

在您的情况下,介质是SSD。虽然SSD内部可以同时将数据写入多个模块(它们可能会达到一种帕勉程度,其中写入速度可能相同,甚至优于读取),但内部映射数据结构是共享资源,因此是有争议的,特别是在频繁更新(如并发写入)上。尽管如此,此数据结构的更新速度非常快,因此除非它成为问题,否则无需担心。

但除此之外,这些只是SSD的内部功能。在外部,您通过串行 ATA 接口进行通信,因此一次一个字节(实际上是帧信息结构 FIS 中的数据包)。除此之外,还有一个操作系统/文件系统,它再次具有可能争用的数据结构和/或应用自己的优化方法,例如写后缓存。

此外,当您知道媒体是什么时,您可能会特别针对此进行优化,并且当单个线程写入大量数据时,SSD的速度非常快。

因此,您可以创建一个大型内存中缓冲区(可能考虑内存映射文件)并同时写入此缓冲区,而不是使用多个线程进行写入。内存本身不会争用,只要您确保每个线程访问其自己的缓冲区地址空间即可。完成所有线程后,将此缓冲区写入 SSD(如果使用内存映射文件,则不需要)。

另请参阅有关SSD开发的精彩摘要:摘要 - 每个程序员都应该了解的有关固态硬盘的信息

进行预分配(或者更确切地说,是 精确映射到 )的要点是,调整文件的大小可能会使用额外的周期,而您可能不会避免这种情况。但同样,这可能取决于操作系统/文件系统。file_.setLength()ftruncate