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
  • on utilise withInvokeExactBehavior().

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)
	  								.withInvokeExactBehavior();
		} 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)
      						.withInvokeExactBehavior();
    } 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. Ses méthodes acceptent différents paramètres sans faire de surcharge... mais par contre,

  • il faut faire un (faux) cast pour le type de retour (et parfois les paramètres)
  • et il faut utiliser withInvokeExactBehavior(),

pour que ce soit réellement efficace.

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)
      						.withInvokeExactBehavior();
    } 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)
			     				.withInvokeExactBehavior();
		} 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;
			}
		}
	}
}