JavaFX application with an ImageIO call on a threa

2020-05-09 05:01发布

问题:

Consider I have a sample JavaFX application which updates its UI with an image read from the application's JAR, and does so in a delayed manner (i.e. the image is painted after the UI is shown):

import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

public final class SameThreadAsync extends Application {
    @Override
    public void start(final Stage primaryStage) {
        final ImageView imageView = new ImageView();
        imageView.setPreserveRatio(true);
        imageView.setSmooth(true);
        imageView.setFitWidth(300.0);
        imageView.setFitHeight(300.0);

        Platform.runLater(() -> {
            final BufferedImage image = getIcon();
            imageView.setImage(SwingFXUtils.toFXImage(image, null));
        });

        final Node label = new Label(null, imageView);

        final StackPane root = new StackPane();
        root.getChildren().add(label);
        primaryStage.setScene(new Scene(root, 300.0, 300.0));
        primaryStage.show();
    }

    private BufferedImage getIcon() {
        System.out.println("Reading an image from thread " + Thread.currentThread().getName());

        try (final InputStream in = new BufferedInputStream(getClass().getResourceAsStream("logo1.png"))) {
            return ImageIO.read(in);
        } catch (final IOException ioe) {
            ioe.printStackTrace();
            return new BufferedImage(0, 0, BufferedImage.TYPE_INT_ARGB);
        }
    }

    public static void main(final String ... args) {
        launch(args);
    }
}

The above code works fine. Now, consider I want to load the image in a separate thread and process the result on the JavaFX Application Thread, so I'll rewrite the code as follows:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class SeparateThreadAsync extends Application {
    private static final ExecutorService IO_EXECUTOR = Executors.newSingleThreadExecutor(r -> new Thread(r, "I/O Queue"));

    @Override
    public void start(final Stage primaryStage) {
        final ImageView imageView = new ImageView();
        imageView.setPreserveRatio(true);
        imageView.setSmooth(true);
        imageView.setFitWidth(300.0);
        imageView.setFitHeight(300.0);

        IO_EXECUTOR.submit(() -> {
            final BufferedImage image = getIcon();
            Platform.runLater(() -> imageView.setImage(SwingFXUtils.toFXImage(image, null)));
        });

        final Node label = new Label(null, imageView);

        final StackPane root = new StackPane();
        root.getChildren().add(label);
        primaryStage.setScene(new Scene(root, 300.0, 300.0));
        primaryStage.show();
    }

    private BufferedImage getIcon() {
        System.out.println("Reading an image from thread " + Thread.currentThread().getName());

        try (final InputStream in = new BufferedInputStream(getClass().getResourceAsStream("logo1.png"))) {
            return ImageIO.read(in);
        } catch (final IOException ioe) {
            ioe.printStackTrace();
            return new BufferedImage(0, 0, BufferedImage.TYPE_INT_ARGB);
        }
    }

    public static void main(final String ... args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            IO_EXECUTOR.shutdown();
            System.out.println("I/O Queue shut down.");
        }));

        launch(args);
    }
}

The rewritten code works fine on both Linux and Windows but hangs on Mac OS X 10.14.5 (Mojave), Oracle JDK 1.8.0_192 and also JetBrains Runtime 1.8.0_202.

The partial thread dump is:

"I/O Queue" #19 prio=5 os_prio=31 tid=0x00007ffe3d85a000 nid=0x12007 runnable [0x0000700004822000]
   java.lang.Thread.State: RUNNABLE
    at java.lang.ClassLoader$NativeLibrary.load(Native Method)
    at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
    - locked <0x000000076ab068a8> (a java.util.Vector)
    - locked <0x000000076ab06900> (a java.util.Vector)
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1824)
    at java.lang.Runtime.load0(Runtime.java:809)
    - locked <0x000000076ab1dea8> (a java.lang.Runtime)
    at java.lang.System.load(System.java:1086)
    at java.lang.ClassLoader$NativeLibrary.load(Native Method)
    at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
    - locked <0x000000076ab068a8> (a java.util.Vector)
    - locked <0x000000076ab06900> (a java.util.Vector)
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1845)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    - locked <0x000000076ab1dea8> (a java.lang.Runtime)
    at java.lang.System.loadLibrary(System.java:1122)
    at java.awt.Toolkit$3.run(Toolkit.java:1636)
    at java.awt.Toolkit$3.run(Toolkit.java:1634)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.awt.Toolkit.loadLibraries(Toolkit.java:1633)
    at java.awt.Toolkit.<clinit>(Toolkit.java:1670)
    at sun.awt.AppContext$2.run(AppContext.java:277)
    at sun.awt.AppContext$2.run(AppContext.java:266)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.awt.AppContext.initMainAppContext(AppContext.java:266)
    at sun.awt.AppContext.access$400(AppContext.java:135)
    at sun.awt.AppContext$3.run(AppContext.java:321)
    - locked <0x000000076c238c00> (a sun.awt.AppContext$GetAppContextLock)
    at sun.awt.AppContext$3.run(AppContext.java:304)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.awt.AppContext.getAppContext(AppContext.java:303)
    at javax.imageio.spi.IIORegistry.getDefaultInstance(IIORegistry.java:154)
    at javax.imageio.ImageIO.<clinit>(ImageIO.java:66)
    at com.example.SeparateThreadAsync.getIcon(SeparateThreadAsync.java:49)
    at com.example.SeparateThreadAsync.lambda$start$2(SeparateThreadAsync.java:33)
    at com.example.SeparateThreadAsync$$Lambda$61/1820086024.run(Unknown Source)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

"JavaFX Application Thread" #15 prio=5 os_prio=31 tid=0x00007ffe3b0e4000 nid=0x307 waiting for monitor entry [0x00007ffee75f1000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.lang.Runtime.load0(Runtime.java:801)
    - waiting to lock <0x000000076ab1dea8> (a java.lang.Runtime)
    at java.lang.System.load(System.java:1086)
    at com.sun.glass.utils.NativeLibLoader.loadLibraryFullPath(NativeLibLoader.java:201)
    at com.sun.glass.utils.NativeLibLoader.loadLibraryInternal(NativeLibLoader.java:94)
    at com.sun.glass.utils.NativeLibLoader.loadLibrary(NativeLibLoader.java:39)
    - locked <0x000000076b4185c0> (a java.lang.Class for com.sun.glass.utils.NativeLibLoader)
    at com.sun.javafx.font.PrismFontFactory.lambda$static$244(PrismFontFactory.java:100)
    at com.sun.javafx.font.PrismFontFactory$$Lambda$70/1988937384.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.font.PrismFontFactory.<clinit>(PrismFontFactory.java:98)
    at com.sun.javafx.text.PrismTextLayout.<clinit>(PrismTextLayout.java:67)
    at com.sun.javafx.text.PrismTextLayoutFactory.<clinit>(PrismTextLayoutFactory.java:33)
    at com.sun.javafx.tk.quantum.QuantumToolkit.getTextLayoutFactory(QuantumToolkit.java:1086)
    at com.sun.javafx.scene.control.skin.Utils.<clinit>(Utils.java:90)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at com.sun.javafx.css.StyleManager.getURL(StyleManager.java:863)
    at com.sun.javafx.css.StyleManager.loadStylesheetUnPrivileged(StyleManager.java:1075)
    - locked <0x000000076c3bc948> (a java.lang.Object)
    at com.sun.javafx.css.StyleManager.loadStylesheet(StyleManager.java:935)
    at com.sun.javafx.css.StyleManager._setDefaultUserAgentStylesheet(StyleManager.java:1395)
    - locked <0x000000076c3bc948> (a java.lang.Object)
    at com.sun.javafx.css.StyleManager.setUserAgentStylesheets(StyleManager.java:1227)
    - locked <0x000000076c3bc948> (a java.lang.Object)
    at com.sun.javafx.application.PlatformImpl.lambda$_setPlatformUserAgentStylesheet$181(PlatformImpl.java:698)
    at com.sun.javafx.application.PlatformImpl$$Lambda$65/566730701.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl._setPlatformUserAgentStylesheet(PlatformImpl.java:697)
    at com.sun.javafx.application.PlatformImpl.setPlatformUserAgentStylesheet(PlatformImpl.java:548)
    at com.sun.javafx.application.PlatformImpl.setDefaultPlatformUserAgentStylesheet(PlatformImpl.java:512)
    at javafx.scene.control.Control.<clinit>(Control.java:87)
    at com.example.SeparateThreadAsync.start(SeparateThreadAsync.java:37)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$161(LauncherImpl.java:863)
    at com.sun.javafx.application.LauncherImpl$$Lambda$56/1315653396.run(Unknown Source)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$174(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl$$Lambda$47/1212899836.run(Unknown Source)
    at com.sun.javafx.application.PlatformImpl.lambda$null$172(PlatformImpl.java:295)
    at com.sun.javafx.application.PlatformImpl$$Lambda$49/1963951195.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$173(PlatformImpl.java:294)
    at com.sun.javafx.application.PlatformImpl$$Lambda$48/1289696681.run(Unknown Source)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)

so it seems like some sort of a race condition. Interestingly, a similar JFC/Swing application doesn't hang at all.

I first encountered this behaviour while playing with coroutines in Kotlin. For example, this code doesn't hang while this one does.

Questions:

  1. Can anyone explain the above behaviour? Is there any chance I'm missing something rather than seeing a bug in the JDK?
  2. How can I further diagnose the problem?

回答1:

I'm writing an application that needs to lazy-load image thumbnails on a separate thread. ImageIO.read() was hanging, too. But someone suggested doing

new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);

in the main application thread before starting other threads, as this apparently causes AWT to run some initialization code the first time which has to be done in the main thread. That solved the ImageIO.read problem.

But it was also hanging when calling

SwingFXUtils.toFXImage(bufferedImage, null);

so I moved that inside my "Platform.runLater(...)" call so that it would be done on the JavaFX thread, and that worked, though it isn't ideal, since I would like that conversion to happen on the separate thread so the UI will be more responsive. But that conversion happens in memory, so I can live with it.



回答2:

The above behaviour seems to be a bug in the Mac OS X implementation of Oracle JDK -- the problem shows up only with Java 1.8.0, I was unable to reproduce it against either Java 9.0.4 or 10.0.2.