package jmri.util; import java.util.concurrent.*; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * This class serves as a demonstration of some good * threading practices in JMRI, and also as a run-time test of them. *

* It's in the java/test package tree because it's not * something we intend to ship to users. *

* For information on threading in JMRI, see the * Threading doc page. * See also the {@link ThreadingUtil} and {@lnk WaitHandler} classes along with the * examples in their associated JUnit test classes: * {@link ThreadingUtilTest} and {@lnk WaitHandlerTest}. * * @author Bob Jacobsen Copyright 2017 */ public class ThreadingDemoAndTest { private volatile boolean flagInterrupted1 = false; private volatile boolean flagInterrupted2 = false; private volatile boolean flagInterrupted3 = false; /** * Show the basic life-cycle of a thread *

* Plus the synchronization needed around the wait and wake-up calls */ @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = {"NN_NAKED_NOTIFY"}, justification = "no actual condition being waited on before notify") @Test public void testThreadingLifeCycle() { final Object lock = new Object(); // this object is the lock for the wait and notify final Thread t = new Thread() { @Override @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = {"WA_NOT_IN_LOOP","UW_UNCOND_WAIT"}, justification = "wait / notify on local final object") public void run() { try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { } } }; t.setName("testThreadingLifeCycle"); t.setDaemon(true); // confirm our understanding of the life cycle assertEquals( Thread.State.NEW, t.getState()); t.start(); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t); }, "Got to wait state"); synchronized(lock) { lock.notifyAll(); } JUnitUtil.waitFor( ()->{ return t.getState().equals(Thread.State.TERMINATED); }, "Got to terminated state"); } /** * How one thread t2 can "join" on the ending of another thread t1 */ @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = {"NN_NAKED_NOTIFY"}, justification = "no actual condition being waited on before notify") @Test public void testThreadingJoinCycle() { final Object lock = new Object(); final Thread t1 = new Thread() { @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = {"WA_NOT_IN_LOOP","UW_UNCOND_WAIT"}, justification = "wait / notify on local final object") @Override public void run() { try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { } } }; t1.setName("testThreadingLifeCycle 1"); t1.setDaemon(true); final Thread t2 = new Thread() { @Override public void run() { try { t1.join(); } catch (InterruptedException e) { } } }; t2.setName("testThreadingLifeCycle 2"); t2.setDaemon(true); // confirm our understanding of the life cycle assertEquals( Thread.State.NEW, t1.getState()); assertEquals( Thread.State.NEW, t2.getState()); t1.start(); t2.start(); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t1); }, "Got 1 to wait state"); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t2); }, "Got 2 to wait state"); synchronized(lock) { lock.notifyAll(); } JUnitUtil.waitFor( ()->{ return t1.getState().equals(Thread.State.TERMINATED); }, "Got 1 to terminated state"); JUnitUtil.waitFor( ()->{ return t2.getState().equals(Thread.State.TERMINATED); }, "Got 2 to terminated state"); } /** * Interrupting a thread ends the current wait, but * doesn't kill the thread; it can go on to wait more. */ @Test public void testInterruptAndContinue() { flagInterrupted1 = false; // set true when we get to the first wait flagInterrupted2 = false; // set true when we get to the second wait final Object lock = new Object(); final Thread t1 = new Thread() { @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = {"WA_NOT_IN_LOOP","UW_UNCOND_WAIT"}, justification = "wait / notify on local final object") @Override public void run() { try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { flagInterrupted1 = true; // we're just going to continue to another wait } try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { flagInterrupted2 = true; // we're just going to continue and terminate } } }; t1.setName("testInterruptAndContinue"); t1.setDaemon(true); // confirm our understanding of the life cycle assertEquals( Thread.State.NEW, t1.getState()); t1.start(); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t1); }, "Got to wait state"); t1.interrupt(); // end 1st wait JUnitUtil.waitFor( ()->{ return flagInterrupted1; }, "handled first interrupt"); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t1); }, "and went to second wait"); t1.interrupt(); // end 2nd wait JUnitUtil.waitFor( ()->{ return flagInterrupted2; }, "handled second interrupt"); JUnitUtil.waitFor( ()->{ return t1.getState().equals(Thread.State.TERMINATED); }, "Got 1 to terminated state"); } /** * Interrupting a thread that restores the interrupted status also kills the next wait */ @Test public void testThreadReassertsInterrupt() { flagInterrupted1 = false; // set true when we leave the first wait flagInterrupted2 = false; // set true when we leave the second wait flagInterrupted3 = false; // set true when we leave the third wait final Object lock = new Object(); final Thread t1 = new Thread() { @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = {"WA_NOT_IN_LOOP","UW_UNCOND_WAIT"}, justification = "wait / notify on local final object") @Override public void run() { try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { flagInterrupted1 = true; Thread.currentThread().interrupt(); // restored the status for the next wait } try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { flagInterrupted2 = true; } try { synchronized(lock) { lock.wait(); } } catch (InterruptedException e) { flagInterrupted3 = true; // we're just going to continue and terminate } } }; t1.setName("testThreadReassertsInterrupt"); t1.setDaemon(true); // confirm our understanding of the life cycle assertEquals( Thread.State.NEW, t1.getState()); t1.start(); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t1); }, "Got to wait state"); t1.interrupt(); // end 1st wait JUnitUtil.waitFor( ()->{ return flagInterrupted1; }, "handled first interrupt"); JUnitUtil.waitFor( ()->{ return flagInterrupted2; }, "continued to second interrupt handler"); // it ran to the 2nd, but waited there because that consumed the interrupt JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t1); }, "at third wait"); assertFalse( flagInterrupted3); t1.interrupt(); // end 3rd wait JUnitUtil.waitFor( ()->{ return flagInterrupted3; }, "continued to third interrupt handler"); JUnitUtil.waitFor( ()->{ return t1.getState().equals(Thread.State.TERMINATED); }, "continued off the end to terminated state"); } /** * Confirm interrupt behavior of blocking queue put */ @Test public void testInterruptBlockingQueuePut() { flagInterrupted1 = false; // set true when we leave the first wait flagInterrupted2 = false; // set true when we leave the second wait flagInterrupted3 = false; // set true when we leave the third wait BlockingQueue q = new ArrayBlockingQueue<>(2); final Thread t = new Thread() { @Override public void run() { try { q.put(1); q.put(2); } catch (InterruptedException e) { fail("did not expect interrupt"); } flagInterrupted1 = true; // third should block until read try { q.put(3); } catch (InterruptedException e) { // just eat and continue flagInterrupted2 = true; } try { q.put(4); } catch (InterruptedException e) { // just eat and continue flagInterrupted3 = true; } try { q.put(5); } catch (InterruptedException e) { // just eat and continue flagInterrupted3 = true; } } }; t.setName("testInterruptBlockingQueuePut"); t.setDaemon(true); // confirm our understanding of the life cycle assertEquals( Thread.State.NEW, t.getState()); t.start(); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t); }, "Got to wait state after adding 2"); assertTrue(flagInterrupted1); assertEquals( 2, q.size()); assertFalse( flagInterrupted2); assertEquals( Integer.valueOf(1), q.poll(), "first"); // should have allowed another JUnitUtil.waitFor( ()->{ return q.size() == 2; }, "Third added"); assertFalse( flagInterrupted2); assertFalse( flagInterrupted3); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t); }, "Got to wait state after adding 3"); JUnitUtil.waitFor( ()->{ return q.size() == 2; }, "Fourth not yet present"); // waiting to add 4, interrupt t.interrupt(); JUnitUtil.waitFor( ()->{ return flagInterrupted3; }, "Interrupt handled in add 4"); // pull contents assertEquals( Integer.valueOf(2), q.poll(), "second"); assertEquals( Integer.valueOf(3), q.poll(), "third"); JUnitUtil.waitFor( ()->{ return t.getState().equals(Thread.State.TERMINATED); }, "Got to terminated state"); assertEquals( Integer.valueOf(5), q.poll(), "fifth; fourth cancelled"); } /** * Confirm interrupt behavior of blocking queue get */ @Test public void testInterruptBlockingQueueGet() { flagInterrupted1 = false; // set true when we leave the first wait flagInterrupted2 = false; // set true when we leave the second wait flagInterrupted3 = false; // set true when we leave the third wait BlockingQueue q = new ArrayBlockingQueue<>(2); final Thread t = new Thread() { @Override public void run() { try { flagInterrupted1 = true; fail(" did not expect to complete: "+q.take()); } catch (InterruptedException e) { flagInterrupted2 = true; } flagInterrupted3 = true; } }; t.setName("testInterruptBlockingQueueGet"); t.setDaemon(true); // confirm our understanding of the life cycle assertEquals( Thread.State.NEW, t.getState()); t.start(); JUnitUtil.waitFor( ()->{ return ThreadingUtil.isThreadWaiting(t); }, "Got to wait input"); assertTrue(flagInterrupted1); assertFalse( flagInterrupted2); assertFalse( flagInterrupted2); t.interrupt(); JUnitUtil.waitFor( ()->{ return flagInterrupted2; }, "Interrupt handled"); JUnitUtil.waitFor( ()->{ return t.getState().equals(Thread.State.TERMINATED); }, "Got to terminated state"); assertTrue(flagInterrupted3); } @BeforeEach public void setUp() { jmri.util.JUnitUtil.setUp(); } @AfterEach public void tearDown() { jmri.util.JUnitUtil.tearDown(); } }