2

I have the following thread:

Thread t1 = new Thread() {
    @Override
    public void run() {
        while (!progress.equals(duration)) {
            try {
                Thread.sleep(1000);
                progress = progress.plusSeconds(1);
                // synchronized (this) { while (paused) { this.wait(); } }
            } catch (InterruptedException e) {
                interrupt();
            }
        }
    }
};
t1.start();

I'm trying to implement a functionality which allows the user to pause and stop this thread using the console. Basically, this:

Scanner sc = new Scanner(System.in);
int choice;

while (t1.isAlive()) {
    System.out.println("Choose an option:\n1. Pause/Resume\n2. Stop");
    choice = Integer.parseInt(sc.nextLine());
    // if (choice == 1) { ... } else if (choice == 2) { t1.interrupt() }
    // synchronized (t1) { t1.notify(); }
}

My problem is that once t1 dies, t1.isAlive() evaluates to false, but the program doesn't exit the while loop because it is stuck waiting for one last input from the user. I want to interrupt sc.nextLine(), but I read it is not possible because the thread is blocked. How could I do this?

I tried the following:

Thread t2;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

while (t1.isAlive()) {
    t2 = new Thread() {
        @Override
        public void run() {
            try {
                while (!br.ready())
                    Thread.sleep(200);
                choice = Integer.parseInt(br.readLine());
            } catch (InterruptedException e) {
            } catch (IOException e) {
            }
        }
    };
    t2.start();
}

Supposedly, this should allow me to interrupt t2, but I must be doing something wrong because it keeps printing Chose an option: 1. Pause/Resume 2. Stop, so I cannot check if it works.

Cristina
  • 25
  • 1
  • 3
  • Well, first the obvious question. Why do things this way when a `ExecutorService` and a `Future` already provide a reliable way to test if a thread is running and stop it if needed? – markspace Mar 27 '21 at 15:25
  • This is my first time working with threads and I started learning to code not so long ago, so I don't know what `ExecutorService` and `Future` do. Would using them complicate my code too much? Because I'm trying to keep it as simple as possible since this is an answer to an exercise which says to use "simple time threads" to solve this. – Cristina Mar 27 '21 at 15:43
  • OK, learning the basics is fine for using language primitives like this. The next question I have is I guess your goal. `t1` doesn't call any `Scanner` methods, so I'm not sure how you expect an interrupt to pause/stop it. I think a little more explanation what you are actually trying to do might help you get answers. Try to include most of the actual exercise so we can see what's really going on. – markspace Mar 27 '21 at 15:53
  • The purpose of the program is: first, a user "logs in" inputing some info (a User instance is created and added to an ArrayList); then, that user is able to "upload" a video (a Video instance is created); finally, the user is able to "play" the video (this is what `t1` simulates, that the video is playing), and they should be able to pause/stop it while it is playing. It works fine, but once the video finishes playing, the program doesn't come out of the `while (t1.isAlive())` loop, it gets stuck waiting for one last input. What I want to know is if there is a simple way to fix this. – Cristina Mar 27 '21 at 17:42
  • Fix this = come out of the while loop without having to input anything else because it makes no sense to do so if the video has finished playing and so that the user can upload another video if they please or play the video again – Cristina Mar 27 '21 at 17:46

2 Answers2

1

The crucial issue is that the API of System.in makes no guarantees. A JVM can fulfill the complete JVM spec even if it has a System.in such that, if it is interrupted, nothing happens and it is in fact completely impossible to interrupt System.in, aside from System.exit.

However, most JVM implementations fortunately don't quite work that way: If you raise the interrupt flag on any given thread, 3 things are going to happen:

  1. Any method that is specced to definitely look at em will be interrupted: These are all methods that are declared to throws InterruptedException. All these methods will, if the thread's interrupt flag is raised, stop waiting immediately, lower the flag, and return by way of throwing InterruptedException. Yes, this means that if you first raise the interrupt flag (someThread.interrupt() raises the flag and doesn't do anything else; it's other methods that look at it that makes the magic work), and then invoke e.g. Thread.sleep, the sleep calls returns immediately (by throwing InterruptedEx) and waits no even a single millisecond.

  2. Methods that pause a thread but which are not specced to definitely deal with it properly are in limboland: It is up to the implementation of the java runtime if anything happens. However, usually something will happen. These methods almost always throw some sort of checked exception (for DB connections, SQLEx, for network, file, and pipe operations, IOException); any code that is currently waiting to send or receive data on one of these things will deal with a raised interrupt flag by lowering the flag, aborting the operation, and returning by way of throwing that checked exception with a message that indicates an interruption occurred.

  3. If code is executing that doesn't respond to the interrupt flag at all, then nothing happens: The flag stays raised and the JVM is not going to do anything else; the point of the interrupt flag is that it just gets raised and then you wait until the thread runs code that looks at it. Hopefully, that will happen very soon, but there are no guarantees.

That means that most likely all you need to do is:

In T1

  1. Have some sort of AtomicBoolean object that will be set to true by t1 once the job is completed.
  2. t1 will also raise the interrupt flag of t2 when the job is completed.

In T2

  1. Protect your readLine() call by putting it in a try/catch block, catching IOException. If there is a loop you may also want to consider checking the interrupt flag yourself, in case it is set in between readLine() invokes; you do this with Thread.interrupted(), which returns true and lowers the flag if the flag is up. Generally, something like while (!Thread.interrupted() && other conditions) { /* main loop here */ }.

  2. In the IOException catch handler, check t1's 'we are done' flag (that AtomicBoolean). If it says 'we are done', then interpret the IOEx as simply being notified that the job is done (so, don't log it anywhere - you were expecting it to happen). If, however, the 'we are done' flag isn't set yet, then that IOException is indicating an actual I/O problem with the input pipe, which can happen of course. You should proceed as normal (which usually means, throw it onwards so that the app crashes with a full log, you can't sanely respond to the input pipe getting I/O issues other than to exit with debug info about what happend). So, just throw that IOException. If you can't, throw new UncheckedIOException(thatIoException); is what you are looking for.

The caveat

Just because it works on your system does not mean it will work anywhere else, unfortunately. As I said, on some VM impls System.in.read() is just not interruptable, period. Nothing you can do, other than extremely drastic steps: Stop being a command line app and show a swing GUI window instead or make it a web app of some sort.

Closing notes

ready() and available() are almost completely useless. They aren't broken, in the sense that they do exactly what their javadoc says these methods do, but if you carefully read that javadoc, you'll realize that what they provide is completely useless. The only real way to know if data is available is to actually attempt to read it, which then leads you into the trap of: Well, on some platforms, that's not interruptable. Yup. Sucks. No reliable solution, in the sense that the API guarantees it'll work on all platforms, is available. 99.5% of all code out there that calls these methods is broken. It is highly unlikely that you'd ever want to call these methods.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
0

It looks like an innocent topic, but actually it's a bit more complicated. When you are reading from the standard input, you usually just end up in a call to the operating system. Which will not return until it has actual input to return with, and has no idea about the interruption mechanism of Java. It's described as a by-product here.

What you can do is providing your own InputStream instead of using System.in directly, and implement its read() method in a way that it goes into System.in.read() only when System.in.available() says so. Until then just repeat the check with some delay, like using Thread.sleep() which is prepared to get interrupted anyway:

public static void main(String[] args) {
    Thread main = Thread.currentThread();
//  try (Scanner sc = new Scanner(System.in)) {
    try (Scanner sc = new Scanner(new InputStream() {
        @Override
        public int read() throws IOException {
            while (System.in.available() == 0)
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
                    throw new IOException();
                }
            return System.in.read();
        }
    })) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ie) {
                }
                main.interrupt();
            }
        }).start();
        String line = sc.nextLine();
        System.out.println(line);
        System.out.println(main.isInterrupted());
    } catch (Exception ex) {
        System.out.println("Time's up, probably. Actual exception: " + ex);
        System.out.println(main.isInterrupted());
    }
}

If you comment the try(Scanner...-})) { block and uncomment the single-line variant, you can try how it doesn't work in itself: you will always have to input something, only the result of System.out.println(main.isInterrupted()); will tell you if you did it in 5 seconds or it took more time.

Side note: in your own attempt you were interrupting the timer thread itself, you need a reference to the other thread instead, here in this example that's the Thread main variable.

tevemadar
  • 12,389
  • 3
  • 21
  • 49