What title says.
Java Scanner
throws IndexOutOfBoundsException
if an asynchronous thread is interrupted while waiting on nextLine()
and another nextLine()
is called.
Here is some code that reproduces the problem.
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
/**
* Test class for asynchronous input
*/
public class MainThreaded
{
public static final int SLEEP_MILLIS = 200;
public static final Object LOCK = new Object();
public static boolean breakIfTrueAsynchronous = false;
public static void main (String[] args) {
BlockingQueue<String> inputQueue = new LinkedBlockingDeque<>();
Scanner scanner = new Scanner(System.in);
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable inputReader = () -> {
while(true) {
String str;
synchronized (scanner) {
str = scanner.nextLine();
}
System.out.println("READ THREAD");
inputQueue.add(str);
}
};
Future<?> task = executor.submit(inputReader);
//run a timer to get the asynchronous error every 3 sec
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (LOCK) {
breakIfTrueAsynchronous = true;
}
}
}, 3000);
System.out.println("Test asynchronous input started, write anything below:");
String input = "";
while(true) { // use ctrl+C to exit
try {
input = inputQueue.poll(SLEEP_MILLIS, TimeUnit.MILLISECONDS);
if(input == null) input = ""; // if timeout elapses, avoid null value
synchronized (LOCK){
if(breakIfTrueAsynchronous){
System.err.println("BREAK RECEIVED");
timer.cancel();
task.cancel(true);
executor.shutdown();
break; // goes to end of main below
}
}
} catch (InterruptedException e) {
System.err.println("INTERRUPT!");
}
if(!input.isEmpty()) {
//use input here
System.out.println("INPUT RECEIVED: " + input);
input = "";
}
}
//this is only executed AFTER breakIfTrueAsynchronous IS TRUE
System.out.println("Task cancelled: " + task.isCancelled());
try{
String str;
synchronized (scanner) {
str = scanner.nextLine();
}
System.out.println("INPUT RECEIVED CORRECTLY: " + str);
}catch (IndexOutOfBoundsException e) {
System.out.println("ERROR. IndexOutOfBoundsException thrown:");
e.printStackTrace();
}
}
}
I would expect the final scanner.nextLine();
to not throw anything and keep reading System.in
as normal.
Instead I got the following stackTrace:
java.lang.IndexOutOfBoundsException: end at
java.base/java.util.regex.Matcher.region(Matcher.java:1553) at
java.base/java.util.Scanner.findPatternInBuffer(Scanner.java:1097) at
java.base/java.util.Scanner.findWithinHorizon(Scanner.java:1800) at
java.base/java.util.Scanner.nextLine(Scanner.java:1658) at
MainThreaded.main(MainThreaded.java:70)
Synchronizing on Scanner
solves the exception issue (thank you to @user207421), but now I don't get why the first task isn't interrupted by task.cancel(true)
.
My working hypothesis now is that the issue lies in the interrupt being received while the thread is waiting on scanner.nextLine()
.
Big thank you to anyone who can sort this out T.T
I solved this by only using the thread for reading input and always using inputQueue.poll()
or inputQueue.take()
to use the inputs in other parts of the code.
Never calling scanner.nextLine()
outside of that single thread prevents synch errors, and the rest is handled by the queue. This way I also never stop the thread until the end of the application.
It should be noted that the thread doesn't stop even on a System.exit()
.
Calling System.exit()
while the thread is waiting on scanner.nextLine()
instead waits for the input before closing the Thread along with the application.
Big thank you to @rzwitserloot for all the info on interruptible methods:
All methods that are in the
java.*
package that are declared withthrows InterruptedException
are guaranteed to be interruptable.readLine()
is not one of those methods - which means it is or isn't interruptable. Depends on the OS, JVM version, vendor, chip, and phase of the moon.
And thanks @Nathan who commented:
Interruption works by checking a flag. Code blocked waiting on i/o cant check a flag