Dans la classe ArrayList :
private Object[] elementData;
private int size;
...
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow(); // renvoie un tab. plus grand
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
Dans le cas où la capacité initiale est fixée et assez grande, grow() n'est jamais appelée.
À partir de là, on peut donner un scénario pour que la taille ne soit pas 20 000.
public class Counter {
private int value;
public void addALot() {
for (var i = 0; i < 10_000; i++) {
value = value + 1; // ce n'est pas une opération atomique!
}
}
public static void main(String[] args) throws InterruptedException {
var counter = new Counter();
var thread1 = Thread.ofPlatform().start(counter::addALot);
var thread2 = Thread.ofPlatform().start(counter::addALot);
thread1.join();
thread2.join();
IO.println(counter.value); // :(
}
}
On veut que 2 threads différents ne puissent pas exécuter le code qui correspond à l'instruction value = value + 1 en même temps.
Et on veut aussi garantir qu'on ne va pas essayer de lire la valeur de value avant que l'écriture précédente soit finie (c'est à dire répercutée en RAM et pas juste gardée dans une variable locale ou en cache).
public class Counter {
private int value;
public void addALot() {
for (var i = 0; i < 10_000; i++) {
// début section critique
value = value + 1;
// fin section critique
}
}
public void add42() {
// début section critique
if (value != 13){
value = value + 42;
}
// fin section critique
}
}
Un seul thread à la fois peut exécuter le code d'une section critique ! Attention, ça ne veut PAS dire que le thread ne peut pas être de-schédulé s'il est dans la section critique.
Et la RAM est mise à jour correctement !
synchronized et lock
public class Counter {
private int value;
private final Object lock;
public Counter() {
lock = new Object();
super();
}
public void addALot() {
for (var i = 0; i < 10_000; i++) {
synchronized(lock){
value = value + 1;
}
}
}
}
L'objet qui sert de lock (on dit aussi moniteur) doit être privé et final, on verra bientôt pourquoi.
synchronized C'est un verrou :
Quand un thread entre dans un bloc synchronized, il prend un jeton associé à l'objet lock et lorsqu'il sort du bloc synchronized, il rend le jeton.
Mais dans ce cas, il le garde et aucun autre thread ne peut le prendre (et donc entrer dans un bloc synchronisé utilisant le même lock).
synchronized C'est une barrière en mémoire entre le blocs synchronisés sur le même lock :
synchronized (sur le même verrou).
Ça impose un certain ordre sur les instructions :
synchronized oblige l'optimiseur de code/JIT à ne pas déplacer des instructions hors du bloc
(mais, à l'intérieur du bloc les instructions peuvent être ré-organisées sans problème).
C'est un objet : ça ne peut être ni un primitif, ni null;
Donc pas de String, de ".class" ou de wrapper, par exemple;
Sinon on risque de créer des blocages.
L'interning est une technique utilisée pour éviter d'allouer plusieurs fois un objet représentant la même chose. Il y en a notamment dans :
java.lang.Integer avec Integer.valueOf ou auto-boxing.
Par exemple :
Object o = 3; // appelle implicitement Integer.valueOf(3)
java.lang.Class avec ".class" ou Class.forName()
On n'utilise pas ces objets en tant que lock pour éviter que le même objet serve de verrou pour des besoins différents.
On empêche que les locks soient visibles hors du code nécessaire et on on utilise le principe d'encapsulation de la POO :
private ;Ces objets ne doivent pas pouvoir servir de verrou en dehors de votre code !
En particulier, on ne doit pas prendre un objet qui est peut déjà être utilisé comme lock (comme System.out, par exemple)
final.L'objet qui sert de lock doit être final car on ne veut pas que la référence de l'objet puisse changer.
Ça ne suffit pas ! Il faut qu'il soit déclaré final. Sinon, il est possible pour un thread de le voir alors qu'il n'est pas encore initialisé (c'est lié au problème de publication que nous verrons plus tard) et dans ce cas l'objet est null.
public class Foo {
private final Object lock;
public Foo() {
lock = new Object();
super();
}
public void bar() {
synchronized(lock){
...
}
}
}
Un même thread peut prendre plusieurs fois le même lock :
public class Foo {
private int value;
private final Object lock;
public Foo() {
lock = new Object();
super();
}
public void setValue(int value) {
synchronized (lock) {
this.value = value;
}
}
public void reset() {
synchronized (lock) {
setValue(0);
}
}
}
public class HelloListFixed {
public static void main(String[] args) {
var nbThreads = 4;
var threads = new Thread[nbThreads];
var list = new ArrayList<Integer>();
var lock = new Object();
IntStream.range(0, nbThreads).forEach(j -> {
threads[j] = Thread.ofPlatform().start(() -> {
for (var i = 0; i < 5000; i++) {
synchronized(lock){
list.add(i);
}
}
});
});
for (var thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
throw new AssertionError();
}
}
IO.println("taille de la liste : " + list.size());
}
}
NON ! Qu'est-ce qui ne va pas dans ce code ?
On appelle thread-safe une classe qui peut être utilisée par plusieurs threads sans avoir d'états incohérents.
Cette définition est nécessairement un peu floue car cela dépend du contrat spécifié par la classe.
Remarque : Un record ou une classe non-mutable est par définition thread-safe (par exemple: java.lang.String).
En ce qui concerne l'API Java :
java.util.HashMap), sauf si c'est explicitement spécifié dans la javadoc (par exemple : java.util.Random).java.util.concurrent sont toutes thread-safe.
public class ThreadSafeList {
private final ArrayList<Integer> list;
private final Object lock;
public ThreadSafeList() {
list = new ArrayList<>();
lock = new Object();
super();
}
public void add(int value) {
synchronized (lock) {
list.add(value);
}
}
public int size() {
synchronized (lock) {
return list.size();
}
}
}
Attention à ne pas oublier le bloc synchronized dans size(). Par exemple, il est nécessaire pour que les modifications du add soient bien relues en RAM.
public class HelloListBetter {
public static void main(String[] args){
var nbThreads = 4;
var threads = new Thread[nbThreads];
var list = new ThreadSafeList();
IntStream.range(0, nbThreads).forEach(j -> {
threads[j] = Thread.ofPlatform().start(() -> {
for (var i = 0; i < 5000; i++) {
list.add(i);
}
});
});
for (var thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
throw new AssertionError();
}
}
IO.println("taille de la liste : " + list.size());
}
}
Attention, une classe thread-safe, c'est un contrat qui vous garantit le comportement des méthodes de cette classe ....
... par contre, l'utilisation d'une classe thread-safe ne garantit pas que le code qui l'utilise est correct en termes de concurrence !
public class ThreadSafeList {
...
public void removeFirst() {
synchronized (lock) {
list.removeFirst();
}
}
}
public static void main(String[] args) {
var list = new ThreadSafeList();
for (int j = 0; j < 2; j++) {
Thread.ofPlatform().start(() -> {
for (var i = 0; i < 100; i++) {
if (list.size() != 0) {
list.removeFirst();
}
}
});
}
for (var i = 0; i < 200; i++) {
list.add(i);
}
}