Concurrence

VarHandle


Problème de Atomic*

La classe AtomicInteger (par exemple) est simple à utiliser mais...

  • c'est gourmand en mémoire : il faut créer un autre objet pour chaque champ volatile.
  • ça nécessite une indirection en mémoire pour chercher la valeur.
  • pour les références, il y a un cast à chaque accès, car le type de la valeur est effacé (par erasure).

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.

Compteur thread-safe avec un 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
		}
	}
}

Compteur avec un 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...

Signature Polymorphique

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);
  }
}

Structures de données concurrentes

Quasiment aucune implantation de structures de données du package java.util.concurrent n'utilise de bloc synchronized...

On utilise :

  • soit des ReentrantLock,
  • soit des algorithmes lock-free avec des volatile et des CAS.

Exemple de la liste chaînée

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);
    }
  }
}    

Liste chaînée lock-free avec 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);
    }
  }
}

Liste chaînée lock-free avec 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;
			}
		}
	}
}