Search code examples
javamultithreadingjava-9filechannel

Is there a way to prevent ClosedByInterruptException?


In the following example, I have one file being used by two threads (in the real example I could have any number of threads)

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class A {
    static volatile boolean running = true;

    public static void main(String[] args) throws IOException, InterruptedException {
        String name = "delete.me";
        new File(name).deleteOnExit();
        RandomAccessFile raf = new RandomAccessFile(name, "rw");
        FileChannel fc = raf.getChannel();

        Thread monitor = new Thread(() -> {
            try {
                while (running) {
                    System.out.println(name + " is " + (fc.size() >> 10) + " KB");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted");
                        Thread.currentThread().interrupt();
                    }
                }
            } catch (IOException e) {
                System.err.println("Monitor thread died");
                e.printStackTrace();
            }
        });
        monitor.setDaemon(true);
        monitor.start();

        Thread writer = new Thread(() -> {
            ByteBuffer bb = ByteBuffer.allocateDirect(32);
            try {
                while (running) {
                    bb.position(0).limit(32);
                    fc.write(bb);

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted");
                        Thread.currentThread().interrupt();
                    }
                }
            } catch (IOException e) {
                System.err.println("Writer thread died");
                e.printStackTrace();
            }
        });

        writer.setDaemon(true);
        writer.start();

        Thread.sleep(5000);
        monitor.interrupt();
        Thread.sleep(2000);
        running = false;
        raf.close();
    }
}

Rather creating a RandomAccessFile and a memory mapping for each thread, I have one file and one memory mapping shared between threads, but there is a catch, if any thread is interrupted the resource is closed.

delete.me is 0 KB
delete.me is 2 KB
delete.me is 4 KB
delete.me is 6 KB
delete.me is 8 KB
Interrupted
Monitor thread died
java.nio.channels.ClosedByInterruptException
    at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
    at sun.nio.ch.FileChannelImpl.size(FileChannelImpl.java:315)
    at A.lambda$main$0(A.java:19)
    at java.lang.Thread.run(Thread.java:748)
Writer thread died
java.nio.channels.ClosedChannelException
    at sun.nio.ch.FileChannelImpl.ensureOpen(FileChannelImpl.java:110)
    at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:199)
    at A.lambda$main$1(A.java:41)
    at java.lang.Thread.run(Thread.java:748)

Is there any way to prevent the FileChannel from being closed just because one thread using it was interrupted?


EDIT What I want to avoid doing is as I suspect it won't work for Java 9+

private void doNotCloseOnInterrupt(FileChannel fc) {
    try {
        Field field = AbstractInterruptibleChannel.class
                .getDeclaredField("interruptor");
        field.setAccessible(true);
        field.set(fc, (Interruptible) thread
                -> Jvm.warn().on(getClass(), fc + " not closed on interrupt"));
    } catch (Exception e) {
        Jvm.warn().on(getClass(), "Couldn't disable close on interrupt", e);
    }
}

BTW The call to fc.size() returns the size as expected with the above hack.


Solution

  • Since you said you want “one memory mapping shared between threads”, there is no such problem at all, as memory mapping is not affect by the closing of a FileChannel. In fact, it’s a good strategy to close the channel as soon as possible, to reduce the resources held by the application.

    E.g.

    static volatile boolean running = true;
    
    public static void main(String[] args) throws IOException {
        Path name = Paths.get("delete.me");
        MappedByteBuffer mapped;
        try(FileChannel fc1 = FileChannel.open(name, READ,WRITE,CREATE_NEW,DELETE_ON_CLOSE)) {
            mapped = fc1.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
        }
        Thread thread1 = new Thread(() -> {
            LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(50));
            while(running && !Thread.interrupted()) {
                LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
                byte[] b = new byte[5];
                mapped.position(4000);
                mapped.get(b);
                System.out.println("read "+new String(b, StandardCharsets.US_ASCII));
            }
        });
        thread1.setDaemon(true);
        thread1.start();
        Thread thread2 = new Thread(() -> {
            byte[] b = "HELLO".getBytes(StandardCharsets.US_ASCII);
            while(running && !Thread.interrupted()) {
                LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
                mapped.position(4000);
                mapped.put(b);
                System.out.println("wrote "+new String(b, StandardCharsets.US_ASCII));
                byte b1 = b[0];
                System.arraycopy(b, 1, b, 0, b.length-1);
                b[b.length-1] = b1;
            }
            mapped.force();
        });
        thread2.setDaemon(true);
        thread2.start();
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
        thread2.interrupt();
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
        running = false;
    

    This demonstrates how the threads can read and write their data after the channel has been closed and interrupting the writing thread does not stop the reading thread.

    If you need to perform FileChannel operations in addition to memory mapped I/O, there is no problem in using multiple FileChannel instances, so closing one channel does not affect the other. E.g.

    static volatile boolean running = true;
    
    public static void main(String[] args) throws IOException {
        Path name = Paths.get("delete.me");
        try(FileChannel fc1 = FileChannel.open(name,READ,WRITE,CREATE_NEW,DELETE_ON_CLOSE);
            FileChannel fc2 = FileChannel.open(name,READ,WRITE)) {
            Thread thread1 = new Thread(() -> {
                LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(50));
                try {
                    MappedByteBuffer mapped = fc1.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
                    while(running && !Thread.interrupted()) {
                        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
                        byte[] b = new byte[5];
                        mapped.position(4000);
                        mapped.get(b);
                        System.out.println("read from map "
                            +new String(b, StandardCharsets.US_ASCII)
                            +", file size "+fc1.size());
                    }
                }catch(IOException ex) {
                    ex.printStackTrace();
                }
            });
            thread1.setDaemon(true);
            thread1.start();
            Thread thread2 = new Thread(() -> {
                byte[] b = "HELLO".getBytes(StandardCharsets.US_ASCII);
                try {
                    MappedByteBuffer mapped = fc2.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
                    fc2.position(4096);
                    try {
                        while(running && !Thread.interrupted()) {
                            LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
                            mapped.position(4000);
                            mapped.put(b);
                            System.out.println("wrote to mapped "
                                +new String(b, StandardCharsets.US_ASCII));
                            byte b1 = b[0];
                            System.arraycopy(b, 1, b, 0, b.length-1);
                            b[b.length-1] = b1;
                            fc2.write(ByteBuffer.wrap(b));
                        }
                    } finally { mapped.force(); }
                }catch(IOException ex) {
                    ex.printStackTrace();
                }
            });
            thread2.setDaemon(true);
            thread2.start();
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            thread2.interrupt();
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
            running = false;
        }
    }
    

    Here, the interruption of one thread does close its channel, but does not affect the other. Further, even when each thread acquires its own MappedByteBuffer from its own channel, changes show through to the other, even without the use of force(). Of course, the latter is defined to be a system dependent behavior, not guaranteed to work on every system.

    But as shown with the first example, you still may create shared buffers from only one of the channels at the start, while performing the I/O operations on a different channel, one per thread, and it doesn’t matter whether and which channels get closed, the mapped buffers are not affected by it.