Как происходит остановка java приложения?
Один из классических способов обработки остановки 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();
    }
  }
try-catch вполне можно использовать для стандартного 15-го (SIGTERM) сигнала (kill pid)
но 9й сигнал перехватить нельзя - ядро системы прибьет процесс сразу, try/catch будет проигнорирован.