Que signifie le mot clé final sur ?
(malgré la similitude de nom, ça n'a rien à voir avec final.)
Opération courante : on souhaite écrire dans un fichier, puis le refermer. Cependant, il faut aussi traiter le cas des exceptions correctement :
PrintStream out = …;
try {
out.println(…);
out.close();
} catch (IOException e) { out.close(); }
Le code de out.close() est dupliqué. Si le code était plus important, ce ne serait pas maintenable.
On peut utiliser la clause finally dans un try/catch pour écrire du code qui sera exécuté obligatoirement :
PrintStream out = …;
try {
out.println(…);
} catch (IOException e) {
} finally { out.close(); }
On utilise cette construction lorsque l'on souhaite libérer une ressource avant de poursuivre le programme, quelle que soit la manière dont il se poursuit (normalement, exception rattrapée, exception propagée).
Que signifie le mot clé static sur ?
class A {
static HashMap<Integer, String> map;
static {
map = new HashMap<>();
map.put(new Integer(1), "A");
map.put(new Integer(2), "B");
}
…
}
Attention les champs statiques sont initialisés au chargement de la classe par la JVM, donc bien avant que le moindre code utilisateur ait été exécuté.
Soit une interface :
interface I extends J {
public R1 meth(T1 t1, …, Tn tn) throws E1, …, Ek
}
on considère une classe C qui implémente I
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
Rob Pike
void f(int x, int y, int* p) {
int z = *p;
x = x + z;
y = y + z;
*p = x + y;
}
lors de l'évaluation de f plusieurs
(sous-)opérations peuvent être effectuées en parallèle
sur un processeur super-scalaire moderne (lesquelles) ?
Un thread (tâche ou fil d'exécution en français) est la plus petite séquence d'instructions manipulable par l'ordonnanceur d'un système d'exploitation. Chaque thread possède sa propre pile d'exécution. Le tas est peut être partiellement partagé.
Un processus est l'exécution d'un programme. Il est usuellement composé de plusieurs threads. Deux processus ne partagent pas d'espace mémoire (et donc pas de variables).
En Java, on peut programmer un thread (sous-processus) en créant une classe qui hérite de la classe Thread. Une telle classe doit redéfinir la méthode public void run() pour y placer le code à éxécuter en parallèle :
class MonThread extends Thread {
MonThread() {
// constructeur
}
@Override
public void run() {
//Code à exécuter en parallèle
}
}
Attention, la méthode run() n'a pas vocation à être appelée directement par le programmeur. Elle est appelée automatiquement par la JVM lorsque le thread démarre.
public class MonProgramme {
…
public static void main() {
MonThread t = new MonThread();
t.start();
…
}
}
La méthode start() rend la main immédiatement. La méthode run() du thread démarré est éxécutée. Si on appelle start() plus d'une fois, l'exception IllegalThreadStateException est levée.
Les méthodes suivantes permettent d'influencer le comportement des threads :
Pour pouvoir coopérer les threads doivent partager des variables et les modifier. Pour éviter les problèmes de race conditions on peut utiliser des méthodes synchronisées :
private int x;
public synchronized void incr() { x++; }
public synchronized void decr() { x--; }
Si deux threads appellent en même temps la méthode incr() ou decr(), les exécutions sont protégées par un verrou.
Sans utilisation de synchronized la situation suivante peu se produire. On suppose x=0 :
→ appel à incr()
→lecture de x, x vaut 0
→calcul x+1, vaut 1
→écriture x = 1
→ appel à decr()
→lecture de x, x vaut 0
→calcul x-1, vaut - 1
→écriture x = -1
Après exécution, x vaut -1. Avec synchronized les appels de méthode sont mis en séquence.
Chaque objet Java (i.e. n'importe quoi qui n'est pas un type
primitif comme int, boolean, …) possède un
verrou interne (intrinsic lock ou monitor
lock), un entier 32 bits stocké dans l'objet.
Lorsque le thread courant veut acquerrir le verrou:
Pour relacher le verrou il suffit de le décrémenter
void f(HashMap map) {
… //traitements longs qui ne dépendent pas de map
synchronized (map) {
// seul le thread courant peut
// exécuter ce bloc
map.put(…);
map.get(…);
map.remove(…);
}
… //autre traitements longs
}
Une utilisation courante des threads est de vouloir effectuer
une tâche à intervalles réguliers (ex: recharger le
contenu d'un fichier toutes les X secondes).
Java fournit les classes Timer
et TimerTask pour ce cas d'utilisation très
courant.
class PeriodicEcho extends TimerTask {
private int counter = 0;
@Override
public void run () {
System.out.println ("Hello numero " + counter);
counter++;
}
}
…
Timer timer = new Timer();
timer.schedule(new PeriodicEcho(), 1000, 4000);//passe en tâche de fond
…
timer.cancel();
La classe contenant l'action doit étendre TimerTask et doit redéfinir
la méthode public void run().
La classe Timer permet de lancer et
interrompre des tâches :
Il est souvent inélégant de créer une classe séparée pour définir une seule méthode. On peut utiliser une classe anonyme :
Thread th = new Thread () { //thread est une classe
@Override
public run () { … }
};
TimerTask tt = new TimerTask () { //TimerTask est une interface
@Override
public run () { … }
};
Un fichier associé à zone mémoire (memory-mapped file) est un fichier auquel le système d'exploitation associe une zone mémoire (un tableau d'octets). On n'accède plus alors dans le fichier par des primitives write() ou read() mais directement en écrivant/lisant dans le tableau. Le système d'exploitation s'occupe alors de détecter les mises à jour en mémoire et de les répercuter sur le fichier.
Le principal avantage de cette technique est la très grande efficacité du procédé, à peine un peu plus coûteux que d'écrire dans un tableau en C, le système d'exploitation s'occupe seul d'écrire les données sur le disque au moment opportun.
C'est la base de toutes les implémentations des couches physiques des bases données modernes. (i.e. une table de PostgreSQL ou Oracle DB est un fichier « mmapé ».
On veut lire un tableau de 100 entiers stocké dans
un fichier tab.data, incrémenter tous les entiers, et
sauver le tableau modifié dans le même fichier
DataInputStream din = new DataInputStream(
new FileInputStream("tab.data"));
int tab[] = new int[100];
for (int i = 0; i < 100; i ++)
tab[i] = din.readInt() + 1;
din.close();
DataOutputStream dout = new DataOutputStream(
new FileOutputStream("tab.data"));
for (int i = 0; i < 100; i ++)
dout.writeInt(tab[i]);
dout.close();
Combien de mémoire utilise-t-on ?
On a donc copié 3 fois les données en mémoire. La primitve mmap permet d'exposer au programmeur les buffer systèmes et donc de se passer de une copie (si le fichier de sortie est différent du fichier d'entrée) ou de deux copies (si le fichier d'entrée est le même que le fichier de sortie)
FileChannel in = FileChannel.open(Paths.get("tab.data"),
StandardOpenOption.READ,
StandardOpenOption.WRITE);
MappedByteBuffer buff = in.map(MapMode.READ_WRITE,0, 400);
in.close();
IntBuffer ibuff = buff.asIntBuffer();
for (int i = 0; i < 100; i ++)
ibuff.put(i, ibuff.get(i) + 1);
Remarques :
Il est parfois souhaitable que les verrous soient associés à une ressource plutot qu'à du code, et synchronized n'est pas forcément adapté. :
Il est possible de poser un verrou (ou plusieurs) sur un fichier. Un verrou peut être :
De plus, on a une granularité fine : on peut vérouiller une portion d'un fichier uniquement (par exemple les octets 20 à 42). On possède trois primitives pour manipuler les verrous :
La classe FileLock représente les verrous,
la méthode .release() permet de les
relacher.
Pour acquérir un verrou sur un fichier, on doit d'abord créer un
objet de la classe FileChannel (par exemple à
partir du nom de fichier). Une fois un tel objet créé, on peut
utiliser les deux méthodes suivantes :
Il existe une variante .lock() (resp. .tryLock()) sans paramètre qui correspond à .lock(0, Long.MAX_VALUE,f alse).