51 votes

L'opérateur + = est-il thread-safe en Java?

J'ai trouvé le code Java suivant.

 for (int type = 0; type < typeCount; type++)
    synchronized(result) {
        result[type] += parts[type];
    }
}
 

result et parts sont double[] .

Je sais que les opérations de base sur les types primitifs sont thread-safe, mais je ne suis pas sûr de += . Si les synchronized ci-dessus sont nécessaires, existe-t-il une meilleure classe pour gérer une telle opération?

66voto

Stephen C Points 255558

Pas de. L' += opération n'est pas thread-safe. Il exige de verrouillage et / ou d'un bon fonctionnement de la chaîne de "passe-avant" les relations de toute expression impliquant la cession à une salle de champ ou un élément de tableau pour être thread-safe.

(Avec un champ déclaré volatile, le "passe-avant" il existe des relations ... mais uniquement sur les opérations de lecture et écriture. L' += opération consiste en une lecture et une écriture. Ce sont individuellement atomique, mais la séquence n'est pas. Et la plupart d'affectation des expressions à l'aide de = impliquent à la fois une ou plusieurs lectures (sur la droite) et une écriture. Cette séquence n'est pas atomique.)

Pour l'histoire complète, lire JLS 17.4 ... ou le chapitre de "la Java de la Simultanéité dans l'Action", par Brian Goetz et coll.

Comme je sais que les opérations de base sur les types primitifs sont thread-safe ...

En fait, c'est une prémisse inexacte:

  • prenons le cas de tableaux
  • considérer que les expressions sont généralement composés d'une séquence d'opérations, et qu'une séquence d'opérations atomiques n'est pas garanti d'être atomique.

Il y a un problème supplémentaire pour l' double type. Le JLS (17.7) dit ceci:

"Pour l'application du langage de programmation Java modèle de mémoire, une seule écriture à un non-volatile long ou double valeur est traitée comme deux écritures: une pour chacune des moitiés de 32 bits. Il peut en résulter une situation où un thread voit les 32 premiers bits d'une valeur de 64 bits à partir d'une écriture, et la seconde de 32 bits à partir d'une autre écriture."

"Les écritures et les lectures de la volatilité de long et double les valeurs sont toujours atomique."


Dans un commentaire, il est demandé:

Donc, ce type que je devrais utiliser pour éviter de synchronisation globale, qui s'arrête à tous les threads à l'intérieur de cette boucle?

Dans ce cas (où vous êtes à la mise à jour d'un double[], il n'y a pas d'alternative à la synchronisation avec des serrures ou primitive mutex.

Si vous avez eu une int[] ou long[] vous pourriez les remplacer par AtomicIntegerArray ou AtomicLongArray et de faire usage de ces classes serrure-mise à jour gratuite. Cependant il n'y a pas d' AtomicDoubleArray classe, ou même un AtomicDouble classe.

(Mise à JOUR - quelqu'un a fait remarquer que la Goyave donne un AtomicDoubleArray classe, de sorte que serait être une option. Bon, en fait.)

Une façon d'éviter un "verrouillage global" et le massif des problèmes de contention peut-être de diviser le tableau en nominal régions, chacune avec son propre cadenas. De cette façon, un thread ne doit bloquer un thread si elles sont à l'aide de la même région de la matrice. (Rédacteur unique / multiples verrous de lecteur pourrait aider aussi ... si la grande majorité des accès sont lit.)

7voto

Aivean Points 2564

Malgré le fait qu'il n'y a pas d' AtomicDouble ou AtomicDoubleArray en java, vous pouvez facilement créer votre propre basée sur AtomicLongArray.

static class AtomicDoubleArray {
    private final AtomicLongArray inner;

    public AtomicDoubleArray(int length) {
        inner = new AtomicLongArray(length);
    }

    public int length() {
        return inner.length();
    }

    public double get(int i) {
        return Double.longBitsToDouble(inner.get(i));
    }

    public void set(int i, double newValue) {
        inner.set(i, Double.doubleToLongBits(newValue));
    }

    public void add(int i, double delta) {
        long prevLong, nextLong;
        do {
            prevLong = inner.get(i);
            nextLong = Double.doubleToLongBits(Double.longBitsToDouble(prevLong) + delta);
        } while (!inner.compareAndSet(i, prevLong, nextLong));
    }
}

Comme vous pouvez le voir, j'utilise Double.doubleToLongBits et Double.longBitsToDouble pour stocker Doubles comme Longs en AtomicLongArray. Ils ont tous deux la même taille en bits, de sorte que la précision n'est pas perdu (sauf pour les -NaN, mais je ne pense pas que c'est important).

Dans Java 8 la mise en œuvre de l' add peut être encore plus facile, que vous pouvez utiliser accumulateAndGet méthode de AtomicLongArray qui a été ajoutée dans java 1.8.

Upd: Il semble que j'ai quasiment re-mise en œuvre de la goyave est AtomicDoubleArray.

6voto

VA31 Points 612

Même le type de données «double» normal n'est pas thread-safe (car il n'est pas atomique) dans les JVM 32 bits, car il nécessite huit octets en Java (ce qui implique des opérations 2 * 32 bits).

3voto

Tagir Valeev Points 14218

Comme il est déjà expliqué, ce code n'est pas thread-safe. Une solution possible pour éviter la synchronisation en Java 8 est l'emploi de nouveaux DoubleAdder de la classe qui est capable de maintenir la somme de double nombre de thread-safe.

Créer la matrice de DoubleAdder objets avant de parallélisation:

DoubleAdder[] adders = Stream.generate(DoubleAdder::new)
                             .limit(typeCount).toArray(DoubleAdder[]::new);

Alors s'accumuler la somme en parallèle des fils comme ceci:

for(int type = 0; type < typeCount; type++) 
    adders[type].add(parts[type]);
}

Enfin obtenir le résultat après parallèle des sous-tâches fini:

double[] result = Arrays.stream(adders).mapToDouble(DoubleAdder::sum).toArray();

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X