Search code examples
javamultithreadingkotlinjavax.imageio

ImageIO - Read diferent files concurrency issue?


I am currently writing a program, which batch processes different images. So I thought that it might be clever to do the operation (Scaling / adding watermark) in parallel.

The problem is I get the following error:

    Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NullPointerException
    at java.base/java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1006)
    at eu.reisihub.soft.watermarking.Main$main$2$2$2$2.invoke(Main.kt:62)
    at eu.reisihub.soft.watermarking.Main$main$2$2$2$2.invoke(Main.kt:19)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.SequencesKt___SequencesKt.count(_Sequences.kt:1006)
    at eu.reisihub.soft.watermarking.Main.main(Main.kt:66)
Caused by: java.lang.NullPointerException
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:488)
    at java.base/java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:603)
    ... 7 more
Caused by: java.lang.NullPointerException
    at java.desktop/java.awt.color.ICC_Profile.intFromBigEndian(ICC_Profile.java:1784)
    at java.desktop/java.awt.color.ICC_Profile.getNumComponents(ICC_Profile.java:1476)
    at java.desktop/sun.java2d.cmm.lcms.LCMSTransform.<init>(LCMSTransform.java:93)
    at java.desktop/sun.java2d.cmm.lcms.LCMS.createTransform(LCMS.java:173)
    at java.desktop/java.awt.color.ICC_ColorSpace.fromRGB(ICC_ColorSpace.java:230)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.setImageData(JPEGImageReader.java:808)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readImageHeader(Native Method)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readNativeHeader(JPEGImageReader.java:723)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.checkTablesOnly(JPEGImageReader.java:347)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.gotoImage(JPEGImageReader.java:493)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readHeader(JPEGImageReader.java:716)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1173)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1153)
    at java.desktop/javax.imageio.ImageIO.read(ImageIO.java:1468)
    at java.desktop/javax.imageio.ImageIO.read(ImageIO.java:1363)
    at eu.reisihub.shot.UtilsKt.readImage(Utils.kt:19)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:18)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:8)
    at eu.reisihub.soft.watermarking.Main$sam$java_util_concurrent_Callable$0.call(Main.kt)
    at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1603)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

please note, that I know that there is a NPE in java.desktop/java.awt.color.ICC_Profile.intFromBigEndian(ICC_Profile.java:1784). And believe me, it is NOT the image.

I tried 2 approaches with Java 8 and Java 10. I have a fixed size thread pool with 6 threads for 4 cores. I am using Kotlin for the implementation and created an extension function fun Path.readImage(): BufferedImage. For people, who don't know Kotlin, for the purpose of this example only, would correspond roughly to public BufferedImage readImage(Path this).

Naive approach Reading the file using Files.newInputStream(this, StandardOpenOption.READ).use { ImageIO.read(it) }. This opens a new InputStream from the Path and tells ImageIO to read the image from this InputStream. Looks fine? Does not work ~every fourth time on Java 8 and roughly 90% on Java 10. BTW: If I wrap a synchronized block around this, it works - always. With 13 seconds to treat 17 images instead of 7 seconds.

In the meantime I updated Gradle from 4.4 to 4.7. Before I configured IntelliJ to use Java 10 and Gradle to use Java 8. Might be the reason for the differences highlighted above. Side note. I always used synchronized(System.err)!

Sophisticated approach Knowing it works when synchronizing everything my method now looks more complicated and like this:

fun Path.readImage(): BufferedImage = Files.newInputStream(this, StandardOpenOption.READ).buffered().use {
    var nStream: ImageInputStream? = null
    var nReader: ImageReader? = null
    synchronized(System.err) {
        nStream = ImageIO.createImageInputStream(it) ?: throw IIOException("Can't create an ImageInputStream!")

        val iter = ImageIO.getImageReaders(nStream)
        if (!iter.hasNext()) {
            throw IIOException("No image nReader found!")
        }
        nReader = iter.next()
    }

    nStream!!.let { stream ->
        nReader!!.let { reader ->
            reader.setInput(stream, true, true)
            try {
                println(reader)
                return reader.read(0, null)
            } finally {
                reader.dispose()
                stream.close()
            }
        }
    }
}

See the println in the code? I know that for every Image a new com.sun.imageio.plugins.jpeg.JPEGImageReaderobject is created.This improves the performance to the 7 seconds I was talking to. Usually does not work 2-4 times and then works 6-10 times.

The stacktrace for the sophisticated version is the following:

Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NullPointerException
    at java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.util.concurrent.FutureTask.get(FutureTask.java:192)
    at eu.reisihub.soft.watermarking.Main$main$1$2$2$2$2.invoke(Main.kt:64)
    at eu.reisihub.soft.watermarking.Main$main$1$2$2$2$2.invoke(Main.kt:20)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.SequencesKt___SequencesKt.count(_Sequences.kt:1006)
    at eu.reisihub.soft.watermarking.Main$main$1.invoke(Main.kt:68)
    at eu.reisihub.soft.watermarking.Main$main$1.invoke(Main.kt:20)
    at eu.reisihub.shot.UtilsKt.measured(Utils.kt:54)
    at eu.reisihub.soft.watermarking.Main.main(Main.kt:24)
Caused by: java.lang.NullPointerException
    at java.awt.color.ICC_Profile.intFromBigEndian(ICC_Profile.java:1782)
    at java.awt.color.ICC_Profile.getNumComponents(ICC_Profile.java:1474)
    at sun.java2d.cmm.lcms.LCMSTransform.<init>(LCMSTransform.java:98)
    at sun.java2d.cmm.lcms.LCMS.createTransform(LCMS.java:173)
    at java.awt.color.ICC_ColorSpace.fromRGB(ICC_ColorSpace.java:218)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.setImageData(JPEGImageReader.java:694)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readImageHeader(Native Method)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readNativeHeader(JPEGImageReader.java:609)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.checkTablesOnly(JPEGImageReader.java:347)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.gotoImage(JPEGImageReader.java:481)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readHeader(JPEGImageReader.java:602)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1059)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1039)
    at eu.reisihub.shot.UtilsKt.readImage(Utils.kt:39)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:16)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:8)
    at eu.reisihub.soft.watermarking.Main$sam$java_util_concurrent_Callable$0.call(Main.kt)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:748)

Where Main:64 is Future#get And Main:68 is kotlin.Sequence#count

Location the Main.kt file: https://github.com/reisi007/Reisishot-Photo-Tools/blob/ea32a605657226edb6140911fd02f77d006163d3/Watermarking/src/main/kotlin/eu/reisihub/soft/watermarking/Main.kt

Location of Utils.kt file: https://github.com/reisi007/Reisishot-Photo-Tools/blob/ea32a605657226edb6140911fd02f77d006163d3/base/src/main/kotlin/eu/reisihub/shot/Utils.kt

I have tried improving the code for several hours and my head stopped spawning new ideas by now. I know that the mistake also happens with 2 pictures. As it does not always fail, the ICC profile must be correct. I am - honestly - conidering that my HDD is not sound and occasionally messes up. I also tried reading the files from my SSD. As per https://stackoverflow.com/a/26300361/1870799 the read method should be thread-safe. I have no idea whats happening. Every image on disk (represented by a Path), gets it's own Callable<Task>. I am only reading those images and writing it to a different folder. So foreach task one file is read, one file is written. These two files are NOT the same, every task has its own files.

The only thing they share is a BufferedImage in PNG format, which is drawn on the read image (watermark). But that happens later. As can be seen in the stacktrace it happens when reading a JPEG image.

at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1039)
at eu.reisihub.shot.UtilsKt.readImage(Utils.kt:39)

If you want to execute the program, the JSON setting, whose path the main app needs, can be created using https://github.com/reisi007/Reisishot-Photo-Tools/blob/ea32a605657226edb6140911fd02f77d006163d3/Watermarking/src/main/kotlin/eu/reisihub/soft/watermarking/SettingsCreator.kt

I appreciate any input. At the current point I do not know what is wrong...

BTW: For some tasks I am using: https://github.com/coobird/thumbnailator


Solution

  • UPDATE 2018-05-23

    @haraldK mentioned in a comment that loading images profiles before Code:

    import java.awt.color.ColorSpace
    import java.awt.color.ICC_Profile
    import java.awt.image.BufferedImage
    import java.nio.file.Path
    import javax.imageio.ImageIO
    
    object ImgLoadUtils {
        init {
            // Load deferred color space profiles to avoid 
            // ConcurrentModificationException due to JDK
            // Use in public static main void or prior to application initialization
            // https://github.com/haraldk/TwelveMonkeys/issues/402
            // https://bugs.openjdk.java.net/browse/JDK-6986863
            // https://stackoverflow.com/questions/26297491/imageio-thread-safety
            ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData();
            ICC_Profile.getInstance(ColorSpace.CS_PYCC).getData();
            ICC_Profile.getInstance(ColorSpace.CS_GRAY).getData();
            ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ).getData();
            ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB).getData();
        }
    
        fun loadImage(p: Path): BufferedImage = ImageIO.read(p.toFile())
    }
    

    SOURCE


    Thanks for @Alex Taylor for the OpenJDK bug link. That assured me that I am not a total idiot.

    Instead of

    fun Path.readImage(): BufferedImage =
        Files.newInputStream(this, StandardOpenOption.READ).use { ImageIO.read(it) }
    

    I am now using

    fun Path.readImage(): BufferedImage =
        ImageIcon(toUri().toURL()).let {
            BufferedImage(it.iconWidth, it.iconHeight, BufferedImage.TYPE_INT_ARGB).apply {
                it.paintIcon(null, createGraphics(), 0, 0)
            }
        }
    

    This is per https://aacsinia.wordpress.com/2010/12/21/java-how-to-create-buffered-image-from-inputstream/. Speed is on par and error is gone [20+ runs]! IMHO ImageIO.read therefore should not be used in a multithreaded environment....