Один из классических способов обработки остановки java-приложения, как сказали выше - Runtime.getRuntime().addShutdownHook. В свою очередь в нем можно подать команду на остановку всем остальным потокам, либо используя флаг "interrupt" класса Thread, либо используя в рабочих потоках предусмотрительно заведенный собственный флаг остановки.
Соответственно, останавливаемые потоки должны либо периодически проверять флаг остановки, либо корректно обрабатывать InterruptedException.
Пример с флагом "interrupt"
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
Thread.getAllStackTraces().keySet().stream()
//Не стоит останавливать поток в котором отрабатывает хук, а тем более пытаться заджойниться
.filter(thread -> !thread.equals(Thread.currentThread()))
//Демоны предполагают безопасное завершение в любой момент, останавливать их не нужно
.filter(thread -> !thread.isDaemon())
//Устанавливаем флаг "interrupt" для всех остальных
.peek(Thread::interrupt)
.peek(thread -> System.out.println(thread.getClass()))
.forEach(thread -> {
try {
thread.join(); //Ждем завершения всех потоков, которым установлен флаг "interrupt"
} catch (InterruptedException e) {
// ...
}
});
}));
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Thread was interrupted once");
//После перехвата исключения флаг "interrupt" возвращается в исходное состояние, поэтому его нужно установить снова
Thread.currentThread().interrupt();
break;
}
}
while (true) {
try {
Thread.sleep(100); // Мы бы зависли здесь, если бы не установили флаг "interrupt" повторно
} catch (InterruptedException e) {
System.out.println("Thread was interrupted");
Thread.currentThread().interrupt();
break;
}
}
while (!Thread.currentThread().isInterrupted()) {
doSomeUninterruptableWork();
}
}