Concurrence

Interruptions et Exceptions


Exceptions

En Java, il y a deux façon de sortir d'un appel de méthode : par un return (explicite ou implicite), ou par la levée d'une exception.

Il existe deux sortes d'exceptions :

  • Les exceptions unchecked (les erreurs du programmeur) : NullPointerException, ArrayIndexOutOfBoundsException, ...
  • Les exceptions checked (dues à une action extérieure) : IOException, InterruptedException, ... et pour lesquelles on doit "agir".

Arrêter un thread

En Java, il n'est pas possible de forcer un thread à s'arrêter (autrement dit, de le tuer).

La seule façon d'arrêter un thread est de lui demander... gentiment, en lui signalant que l'on souhaiterait qu'il s'arrête.

Mécanisme de signalisation

Un thread peut envoyer un signal d'interruption à un autre thread : autreThread.interrupt().

L'autre thread peut recevoir le signal de 2 façon différentes :

  • statut d'interruption : un (flag) booléen est positionné.
    • Et on peut tester sa valeur avec Thread.interrupted(). Cette méthode est vraiment mal nommée et devrait s'appeler interruptedAndClear(), car elle repositionne également le statut d'interruption à false après son appel !
  • voir la suite...

Exemple (1/2)

var thread = Thread.ofPlatform().start(() -> {
	for (var l = 0; l < 1_000_000_000L; l++) {
		// ici, on fait un calcul plus ou moins lent
	}
});
...
thread.interrupt();	

Dans ce cas, le programme ne sait pas qu'il a été interrompu...

Exemple (2/2)

Il faut tester le statut d'interruption !

var thread = Thread.ofPlatform().start(() -> {
	for (var l = 0; l < 1_000_000_000L && !Thread.interrupted(); l++) {
		// ici, on fait un calcul plus ou moins lent
	}
});
...
thread.interrupt();	

Appel de méthode bloquant

Un appel de méthode est bloquant s'il met le thread courant en attente.

Par exemple :

  • les appels systèmes pour faire des entrées/sorties,
  • l'attente d'un certain temps avec Thread.sleep(),
  • ou l'attente dans un appel à wait().

Ces méthodes devraient déclarer une (IO)InterruptedException... Mais il faut vérifier la doc. Par exemple, les méthodes de Scanner ne le font pas.

Mécanisme de signalisation

Un thread peut envoyer un signal d'interruption à un autre thread : autreThread.interrupt().

L'autre thread peut recevoir le signal de 2 façon différentes :

  • Si le thread n'est pas dans un appel bloquant : un (flag) booléen est positionné. Et on peut tester sa valeur avec Thread.interrupted().
  • Si le thread est dans un appel bloquant, ou si un appel bloquant arrive après : l'exception (checked) InteruptedException est levée (et le statut est positionné à false).

Exemple avec un appel bloquant (1/2)

L'appel à sleep peut provoquer une InterruptedException qui est une exception checked, il faut dont obligatoirement la gérer. Et comme on est dans une lambda, on ne peut pas faire de throws.

var thread = Thread.ofPlatform().start(() -> {
	for(;;){
	  try {
	    Thread.sleep(5_000); // méthode bloquante
	  } catch (InterruptedException e) {
		// ici, il faut IMPÉRATIVEMENT traiter l'exception 
	}
  }
});
...
thread.interrupt();

Attention, un catch qui ne fait rien, cela veut dire que l'on ne pourra jamais arrêter le thread. Donc ce code est mauvais !

Exemple avec un appel bloquant (2/2)

Il y a deux façon de traiter cette InterruptedException :

  • Sortir du main ou de runnable.run() avec un return;
  • Lever une unchecked exception pour sortir de la méthode run() du Runnable.
var thread = Thread.ofPlatform().start(() -> {
	for(;;){
	 try {
		Thread.sleep(5_000); // méthode bloquante
	 } catch (InterruptedException e) {
		return; // ou lever une exception runtime...
	 }
	}
});
...
thread.interrupt();

Avec et sans appel bloquant

Avec un appel bloquant suivi d'un calcul lent :

var thread = Thread.ofPlatform().start(() -> {
	var sum = 0;
	while (true) { 
		try {
			Thread.sleep(5_000); // méthode bloquante
		} catch (InterruptedException e) {
			return;
		}
		sum += findPrime(); // calcul sans appel bloquant mais lent	
	
});
...
thread.interrupt();

Quel que soit le moment où l'interruption est demandée, le prochain appel bloquant lève une InterruptedException.

Opérations d'entrée/sortie

Une partie des opérations d’entrée/sortie sont des appels bloquants : Reader.read(), OutputStream.write(), ...

  • Ils lèvent des IOException;
  • ne lèvent pas d'InterruptedException;
  • lèvent des IOInterruptedException (sous-classe de IOException);

Donc, il ne faut pas oublier de les traiter, et lorsqu'on reçoit une IOInterruptedException, il faut tout fermer et arrêter le thread (ou pour un serveur, arrêter la connexion avec le client, etc..)

L'entrée d'un bloc synchronized

Essayer d'entrer dans un bloc synchronized est une opération bloquante si un autre thread a déjà pris le jeton associé au lock de ce bloc. Attention, elle ne lève pas d'InterruptedException...

S'il y a un risque d'attente (en cas de forte contention, par exemple) et que l'on veut permettre l'interruption de l'entrée dans une section critique, il faut utiliser des ReentrantLock et la méthode lockInterruptibly().

En résumé...

Habituellement, appeler interrupt() sur un thread veut dire qu'on lui demande de s'arrêter.

Mais ce n'est qu'une convention, le code du thread peut faire ce que bon lui semble (pourvu que cela soit écrit dans la javadoc).

Donc l’interruption d'un thread se fait de façon coopérative : le code du thread doit être écrit pour qu'il puisse s'interrompre (et ne rien faire, ou appeler printStackTrace n'est jamais la bonne solution !).

On distingue deux cas :

  • Appel de méthode bloquant : levée d'une InterruptedException.
  • Sinon : vérification du statut avec Thread.interrupted().