Atomic*
La classe AtomicInteger
(par exemple) est simple à utiliser mais...
La classe java.lang.invoke.VarHandle
permet
de palier ces problèmes avec une API moins simple...
VarHandle
Un VarHandle
correspond à un "pointeur" (une handle) sur un
champ volatile
ou une case d'un tableau (aussi volatile
).
Lookup lookup = MethodHandles.lookup(); VarHandle handle = lookup.findVarHandle( SafeCounter.class, // classe contenant le champ "counter", // nom du champ int.class); // type du champ
Lookup.findVarHandle()
permet de créer un VarHandle
à
partir d'un objet Lookup
correspondant à un contexte de sécurité.
MethodHandles.lookup()
permet de demander le contexte de
sécurité à l'endroit de l'appel.
API des VarHandle
getVolatile(object)
permet d’accéder à la valeur du champ à partir
d'une référence sur l'objet.setVolatile(object, value)
change la valeur du champ.compareAndSet(object, oldValue, newValue)
fait un CAS.class Foo { volatile int field; // volatile n'est pas obligatoire // mais c'est mieux ! } ... var handle = lookup.findVarHandle(Foo.class, "field", int.class); var foo = new Foo(); handle.setVolatile(foo, 17); // écriture volatile
Attention, get()
et set()
ne font pas des accès volatiles !
VarHandle sur des cases de tableau
La méthode MethodHandles.arrayElementVarHandle
permet d'obtenir
un VarHandle
sur les cases d'un tableau.
VarHandle arrayHandle = MethodHandles.arrayElementVarHandle(String[].class); ... arrayHandle.setVolatile(array, index, value);
Le premier argument des méthodes du VarHandle
est le tableau,
le second est l'index de la case.
VarHandle
Pour des raisons de performances, on stocke le VarHandle
dans une constante :
public class SafeCounter { private volatile int counter; private static final VarHandle COUNTER_HANDLE; static { var lookup = MethodHandles.lookup(); try { COUNTER_HANDLE = lookup.findVarHandle(SafeCounter.class, "counter", int.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new AssertionError(e); } } public int nextValue() { for (;;) { int current = this.counter; // lecture volatile if (COUNTER_HANDLE.compareAndSet(this, current, current + 1)) { return current; } // otherwise retry } } }
getAndAdd
La méthode getAndAdd()
prend en paramètre la valeur de l'incrément :
class SafeCounter { private volatile int counter; private static final VarHandle COUNTER_HANDLE; static { var lookup = MethodHandles.lookup(); try { COUNTER_HANDLE = lookup.findVarHandle(SafeCounter.class, "counter", int.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new AssertionError(e); } } public int nextValue() { return (int) COUNTER_HANDLE.getAndAdd(this, 1); } public int previousValue() { return (int) COUNTER_HANDLE.getAndAdd(this, -1); } }
On peut remarquer des casts bizarres...
La classe VarHandle
n'utilise pas les types paramétrés.
Les méthodes de VarHandle
acceptent différents paramètres sans faire de surcharge...
mais par contre, il faut faire un (faux) cast pour indiquer le type de retour.
public class SafeCounter { private volatile long counter; private static final VarHandle COUNTER_HANDLE; static { var lookup = MethodHandles.lookup(); try { COUNTER_HANDLE = lookup.findVarHandle(SafeCounter.class, "counter", long.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new AssertionError(e); } } public long nextValue() { return (long) COUNTER_HANDLE.getAndAdd(this, 1L); } }
Quasiment aucune implantation de
structures de données du
package java.util.concurrent
n'utilise de
bloc synchronized
...
On utilise :
ReentrantLock
,volatile
et des CAS
.Cette classe n'est pas thread-safe.
public class LinkedList<E> { private record Link<E>(E value, Link<E> next) {} private Link<E> head; public void addFirst(E value) { Objects.requireNonNull(value); head = new Link<>(value, head); } public void forEach(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); for (var current = head; current != null; current = current.next) { consumer.accept(current.value); } } }
AtomicReference
public class LinkedListThreadSafe<E> { ... private final AtomicReference<Link<E>> head = new AtomicReference<>(); public void addFirst(E value) { Objects.requireNonNull(value); for(;;) { var oldHead = head.get(); // lecture volatile var newHead = new Link<>(value, oldHead); if (head.compareAndSet(oldHead, newHead)) { // lecture/écriture volatile return; } } } public void forEach(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); for( var current = head.get(); current!=null; current = current.next ) { // lecture volatile consumer.accept(current.value); } } }
VarHandle
public class LinkedListVarHandle<E> { ... private volatile Link<E> head; private static final VarHandle HEADER_HANDLE; static { var lookup = MethodHandles.lookup(); try { HEADER_HANDLE = lookup.findVarHandle(LinkedListVarHandle.class, "head", Link.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new AssertionError(e); } } public void addFirst(E value) { Objects.requireNonNull(value); for (;;) { var oldHead = head; // lecture volatile var newHead = new Link<>(value, oldHead); if (HEADER_HANDLE.compareAndSet(this, oldHead, newHead)) { // lecture/écriture volatile return; } } } }