【问题标题】:Concurrency of RandomAccessFile in JavaJava 中 RandomAccessFile 的并发性
【发布时间】:2017-07-30 04:07:06
【问题描述】:

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

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

其中file_RandomAccessFile 的一个实例,buffer 是一个直接字节缓冲区。

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

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

【问题讨论】:

  • 这个问题不同,因为我使用getChannel接口在特定位置写入而不是更改文件的当前位置
  • 是的,它们已经足够不同了,这就是为什么我没有使用强大的 Mjölnir 来结束这个问题,但是在另一个问题中存在一些内容,并且它的答案在这里具有相关性。
  • FileChannel 的文档中,您的问题自己回答了,这取决于操作系统的实现:“文件通道可以安全地供多个并发线程使用。[...] 其他操作,特别是那些采取明确立场的人可能会同时进行;他们是否真的这样做取决于底层的实现,因此是未指定的。”

标签: java multithreading randomaccessfile filechannel


【解决方案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 分区)(基于 Debian 的发行版),带有 Linux Kernel 4.8.0:执行期间线程混合

又如documentation 所说:

文件通道可供多个并发线程安全使用。这 可以随时调用 close 方法,由 Channel 指定 界面。仅涉及通道位置的一项操作或 可以在任何给定时间更改其文件的大小; 尝试在第一个仍然存在时启动第二个此类操作 in progress 将阻塞,直到第一个操作完成。其他 操作,特别是那些采取明确立场的操作,可能 同时进行; 他们是否真的这样做取决于 底层实现,因此未指定。

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

编辑:正如所指出的,上述并不意味着线程可以并发写入文件,它实际上与write调用的行为相反,根据@的约定987654322@ 明确指定一次只有一个线程可以写入给定文件:

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

【讨论】:

  • 是的,但我的问题是,由于写入特定位置总是会改变文件的长度(除非它在文件的当前 eof 之前),所有此类操作不会总是被序列化(与底层实现无关)?
  • @user1715122 EOF 并不是什么大问题,因为文件会增长以适应大于文件大小的缓冲区。如果“序列化”是指在任何给定时间只有一个线程可以写入,那么答案是肯定的,因为来自FileChannelwrite 调用的行为类似于WritableByteChannel
【解决方案2】:

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

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

在您的情况下,媒体是 SSD。 虽然 SSD 内部可能会同时将数据写入多个模块(它们可能会达到写入可能与读取一样快甚至优于读取的并行度),但内部映射数据结构是共享资源,因此会发生争用,尤其是在频繁更新(例如并发)时写道。尽管如此,这个数据结构的更新还是相当快的,因此除非成为问题,否则无需担心。

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

此外,由于您知道您的媒体是什么,您可以专门针对该媒体进行优化,当单个线程写入大量数据时,SSD 的速度非常快。

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

另请参阅有关 SSD 开发的精彩总结: A Summary – What every programmer should know about solid-state drives

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

【讨论】:

    猜你喜欢
    • 2012-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-24
    • 1970-01-01
    • 1970-01-01
    • 2017-07-18
    • 2023-03-18
    相关资源
    最近更新 更多