web-dev-qa-db-fra.com

Existe-t-il un moyen d'empêcher ClosedByInterruptException?

Dans l'exemple suivant, j'ai un fichier utilisé par deux threads (dans l'exemple réel, je pourrais avoir n'importe quel nombre de 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();
    }
}

Plutôt créer un RandomAccessFile et un mappage de mémoire pour chaque thread, j'ai un fichier et un mappage de mémoire partagés entre les threads, mais il y a un hic, si un thread est interrompu, la ressource est fermée.

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)

Existe-t-il un moyen d'empêcher la fermeture de FileChannel simplement parce qu'un thread l'utilisant a été interrompu?


EDIT Ce que je veux éviter de faire est que je soupçonne que cela ne fonctionnera pas pour 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 L'appel à fc.size() renvoie la taille comme prévu avec le hack ci-dessus.

24
Peter Lawrey

Puisque vous avez dit que vous vouliez "un mappage de mémoire partagé entre les threads", il n'y a pas de problème du tout, car le mappage de mémoire n'est pas affecté par la fermeture d'un FileChannel. En fait, c'est une bonne stratégie de fermer la chaîne le plus tôt possible, afin de réduire les ressources détenues par l'application.

Par exemple.

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;

Cela montre comment les threads peuvent lire et écrire leurs données après la fermeture du canal et l'interruption du thread d'écriture n'arrête pas le thread de lecture.

Si vous devez effectuer des opérations FileChannel en plus des E/S mappées en mémoire, il n'y a aucun problème à utiliser plusieurs instances FileChannel, donc la fermeture d'un canal n'affecte pas l'autre. Par exemple.

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

Ici, l'interruption d'un thread ferme son canal, mais n'affecte pas l'autre. De plus, même lorsque chaque thread acquiert son propre MappedByteBuffer à partir de son propre canal, les changements se répercutent sur l'autre, même sans l'utilisation de force(). Bien sûr, ce dernier est défini comme un comportement dépendant du système, non garanti pour fonctionner sur tous les systèmes.

Mais comme indiqué dans le premier exemple, vous pouvez toujours créer des tampons partagés à partir d'un seul des canaux au début, tout en effectuant les opérations d'E/S sur un canal différent, un par thread, et peu importe si et quels canaux se ferme, les tampons mappés ne sont pas affectés par celui-ci.

13
Holger

Vous pouvez utiliser la réflexion pour accéder au champ interruptorillégalement et obtenir le Sun.nio.ch.Interruptible type de classe à partir de là pour créer une instance de proxy:

private void doNotCloseOnInterrupt(FileChannel fc) {
    try {
        Field field = AbstractInterruptibleChannel.class.getDeclaredField("interruptor");
        Class<?> interruptibleClass = field.getType();
        field.setAccessible(true);
        field.set(fc, Proxy.newProxyInstance(
                interruptibleClass.getClassLoader(), 
                new Class[] { interruptibleClass },
                new InterruptibleInvocationHandler()));
    } catch (final Exception e) {
        Jvm.warn().on(getClass(), "Couldn't disable close on interrupt", e);
    }
}

public class InterruptibleInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {
        // TODO: Check method and handle accordingly
        return null;
    }
}

En Java9, cela fonctionne avec un seul avertissement, car il s'exécute par défaut avec --illegal-access=permit.

Cependant, cet indicateur pourrait être supprimé dans les versions futures et la meilleure façon de garantir que cela fonctionne à long terme consiste à utiliser l'indicateur --add-opens:

--add-opens Java.base/Sun.nio.ch=your-module
--add-opens Java.base/Java.nio.channels.spi=your-module

Ou, si vous ne travaillez pas avec des modules (non recommandé):

--add-opens Java.base/Sun.nio.ch=ALL-UNNAMED
--add-opens Java.base/Java.nio.channels.spi=ALL-UNNAMED

Cela fonctionne avec Java 9, Java 10 et la version actuelle du JDK 11 Early-Access Build (28 (2018/8/23))).

10
Max Vollmer

En utilisant un AsynchronousFileChannel puis ClosedByInterruptException n'est jamais levé Il ne semble tout simplement pas se soucier de l'interruption

Test effectué à l'aide de jdk 1.8.0_72

import Java.io.File;
import Java.io.IOException;
import Java.nio.ByteBuffer;
import Java.nio.channels.AsynchronousFileChannel;
import Java.nio.channels.CompletionHandler;
import Java.nio.file.Path;
import Java.nio.file.StandardOpenOption;
import Java.util.concurrent.atomic.AtomicLong;

public class A {
    static volatile boolean running = true;

    public static void main(String[] args) throws IOException, InterruptedException {
        String name = "delete.me";
        Path path = new File(name).toPath();
        AtomicLong position = new AtomicLong(0);

        AsynchronousFileChannel fc = AsynchronousFileChannel.open(path, 
                StandardOpenOption.CREATE_NEW, StandardOpenOption.DELETE_ON_CLOSE ,
                StandardOpenOption.READ, StandardOpenOption.WRITE,
                StandardOpenOption.WRITE, StandardOpenOption.SYNC);

        CompletionHandler<Integer, Object> handler =
                new CompletionHandler<Integer, Object>() {
                @Override
                public void completed(Integer result, Object attachment) {
                    //System.out.println(attachment + " completed with " + result + " bytes written");
                    position.getAndAdd(result);
                }
                @Override
                public void failed(Throwable e, Object attachment) {
                    System.err.println(attachment + " failed with:");
                    e.printStackTrace();
                }
            };

        Runnable monitorRun = () -> {
            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();
                        System.out.println("Interrupt call failed so return");
                        return;
                    }
                }
            } catch (IOException e) {
                System.err.println("Monitor thread died");
                e.printStackTrace();
            }
        };

        Thread monitor = new Thread(monitorRun);
        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,position.get(),null,handler);

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

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

        Thread.sleep(5000);
        monitor.interrupt();
        Thread.sleep(2000);
        monitor = new Thread(monitorRun);
        monitor.start();
        Thread.sleep(5000);
        running = false;
        fc.close();
    }
}

Générez la sortie suivante:

delete.me is 0 KB
delete.me is 3 KB
delete.me is 6 KB
delete.me is 9 KB
delete.me is 12 KB
Interrupted
Interrupt call failed so return
delete.me is 21 KB
delete.me is 24 KB
delete.me is 27 KB
delete.me is 30 KB
delete.me is 33 KB
7
Mumrah81