diff --git a/src/main/java/org/scijava/thread/DefaultThreadService.java b/src/main/java/org/scijava/thread/DefaultThreadService.java index 626fb7a24..2ae6acde7 100644 --- a/src/main/java/org/scijava/thread/DefaultThreadService.java +++ b/src/main/java/org/scijava/thread/DefaultThreadService.java @@ -73,8 +73,17 @@ public final class DefaultThreadService extends AbstractService implements private boolean disposed; + private LifeCounter lifeCounter = new LifeCounter(); + // -- ThreadService methods -- + public DefaultThreadService() { + // NB: We need to call EventQueue.isDispatchThread() once before the JVM + // shuts down. Otherwise EventQueue.isDispatchThread won't work as + // expected during JVM shutdown. + EventQueue.isDispatchThread(); + } + @Override public Future run(final Callable code) { if (disposed) return null; @@ -173,7 +182,9 @@ public synchronized void dispose() { @Override public Thread newThread(final Runnable r) { final String threadName = contextThreadPrefix() + nextThread++; - return new Thread(r, threadName); + Thread thread = new Thread(r, threadName); + thread.setDaemon(true); + return thread; } // -- Helper methods -- @@ -203,6 +214,7 @@ private synchronized void initExecutor() { } private Runnable wrap(final Runnable r) { + lifeCounter.increase(); final Thread parent = Thread.currentThread(); return () -> { final Thread thread = Thread.currentThread(); @@ -212,11 +224,13 @@ private Runnable wrap(final Runnable r) { } finally { if (parent != thread) parents.remove(thread); + lifeCounter.decrease(); } }; } private Callable wrap(final Callable c) { + lifeCounter.increase(); final Thread parent = Thread.currentThread(); return () -> { final Thread thread = Thread.currentThread(); @@ -226,6 +240,7 @@ private Callable wrap(final Callable c) { } finally { if (parent != thread) parents.remove(thread); + lifeCounter.decrease(); } }; } diff --git a/src/main/java/org/scijava/thread/LifeCounter.java b/src/main/java/org/scijava/thread/LifeCounter.java new file mode 100644 index 000000000..ad05cc7a1 --- /dev/null +++ b/src/main/java/org/scijava/thread/LifeCounter.java @@ -0,0 +1,80 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2020 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.thread; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Keeps the JVM alive, if and only if the life count is greater than zero. + *

+ * This is achieved by adding a shutdown hook that waits for the life count to + * reach zero. + * + * @author Matthias Arzt + */ +class LifeCounter { + + private final AtomicInteger counter = new AtomicInteger(); + + private CountDownLatch latch = null; + + /** + * Creates a {@link LifeCounter}, with life count initialized to 0. + * The life counter, will keep the JVM alive as long as the life count + * id greater than zero. + */ + public LifeCounter() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + latch = new CountDownLatch(1); + if (counter.get() > 0) try { + latch.await(); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + })); + } + + /** + * Increases the life count by one. + */ + public void increase() { + counter.incrementAndGet(); + } + + /** + * Decrease the life count by one. + */ + public void decrease() { + if (counter.decrementAndGet() <= 0) + if (latch != null) latch.countDown(); + } +} diff --git a/src/test/java/org/scijava/thread/DefaultThreadServiceShutdownDemo.java b/src/test/java/org/scijava/thread/DefaultThreadServiceShutdownDemo.java new file mode 100644 index 000000000..eb297a30d --- /dev/null +++ b/src/test/java/org/scijava/thread/DefaultThreadServiceShutdownDemo.java @@ -0,0 +1,86 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2020 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.thread; + +import org.scijava.Context; +import org.scijava.command.Command; +import org.scijava.command.CommandService; +import org.scijava.plugin.Parameter; + +import java.util.concurrent.ExecutionException; + +/** + * Demonstrates that the {@link DefaultThreadService} keeps the JVM alive + * exactly as long as required. + *

+ * This is done by using the CommandService which internally calls the + * ThreadService to executes commands. The command, used as an example, + * recursively calls itself, to perform a countdown. + *

+ * The rather complicated countdown example demonstrates, that everything works + * correctly, even for complicated commands that call sub commands. + * + * @author Matthias Arzt + */ +public class DefaultThreadServiceShutdownDemo { + + public static void main(String... args) + throws ExecutionException, InterruptedException + { + Context context = new Context(); + CommandService command = context.service(CommandService.class); + command.run(CountDownCommand.class, true, "count", 10); + } + + public static class CountDownCommand implements Command { + + @Parameter + private int count; + + @Parameter + private CommandService commandService; + + @Override + public void run() { + try { + if(count > 0) { + System.out.println("Count down: " + count); + Thread.sleep(1000); + commandService.run(CountDownCommand.class, true, "count", count - 1); + } else { + System.out.println("Hurray!"); + } + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +}