Comment la machine virtuelle Java effectue la synchronisation des threads

Tous les programmes Java sont compilés dans des fichiers de classe, qui contiennent des bytecodes, le langage machine de la machine virtuelle Java. Cet article examine comment la synchronisation des threads est gérée par la machine virtuelle Java, y compris les bytecodes appropriés. (1750 mots)

Under The Hood de ce mois-ci examine la synchronisation des threads à la fois dans le langage Java et dans la machine virtuelle Java (JVM). Cet article est le dernier d'une longue série d'articles sur le bytecode que j'ai commencé l'été dernier. Il décrit les deux seuls opcodes directement liés à la synchronisation des threads, les opcodes utilisés pour entrer et sortir des moniteurs.

Threads et données partagées

L'une des forces du langage de programmation Java est sa prise en charge du multithreading au niveau du langage. Une grande partie de cette assistance est centrée sur la coordination de l'accès aux données partagées entre plusieurs threads.

La machine virtuelle Java organise les données d'une application Java en cours d'exécution dans plusieurs zones de données d'exécution: une ou plusieurs piles Java, un tas et une zone de méthodes. Pour un aperçu de ces zones de mémoire, consultez le premier article Under the Hood : «La machine virtuelle maigre et moyenne».

Dans la machine virtuelle Java, chaque thread se voit attribuer une pile Java , qui contient des données auxquelles aucun autre thread ne peut accéder, y compris les variables locales, les paramètres et les valeurs de retour de chaque méthode invoquée par le thread. Les données sur la pile sont limitées aux types primitifs et aux références d'objet. Dans la JVM, il n'est pas possible de placer l'image d'un objet réel sur la pile. Tous les objets résident sur le tas.

Il n'y a qu'un seul tas dans la JVM, et tous les threads le partagent. Le tas ne contient que des objets. Il n'y a aucun moyen de placer un type primitif solitaire ou une référence d'objet sur le tas - ces éléments doivent faire partie d'un objet. Les tableaux résident sur le tas, y compris les tableaux de types primitifs, mais en Java, les tableaux sont également des objets.

Outre la pile Java et le tas, les autres données de lieu peuvent résider dans la JVM est la zone de méthode , qui contient toutes les variables de classe (ou statiques) utilisées par le programme. La zone de méthode est similaire à la pile en ce qu'elle ne contient que des types primitifs et des références d'objet. Contrairement à la pile, cependant, les variables de classe dans la zone de méthode sont partagées par tous les threads.

Verrous d'objet et de classe

Comme décrit ci-dessus, deux zones de mémoire de la machine virtuelle Java contiennent des données partagées par tous les threads. Ceux-ci sont:

  • Le tas, qui contient tous les objets
  • La zone de méthode, qui contient toutes les variables de classe

Si plusieurs threads doivent utiliser les mêmes objets ou variables de classe simultanément, leur accès aux données doit être correctement géré. Sinon, le programme aura un comportement imprévisible.

Pour coordonner l'accès aux données partagées entre plusieurs threads, la machine virtuelle Java associe un verrou à chaque objet et classe. Un verrou est comme un privilège qu'un seul thread peut «posséder» à la fois. Si un thread souhaite verrouiller un objet ou une classe en particulier, il demande à la JVM. À un moment donné après que le thread demande un verrou à la JVM - peut-être très bientôt, peut-être plus tard, peut-être jamais - la JVM donne le verrou au thread. Lorsque le thread n'a plus besoin du verrou, il le renvoie à la JVM. Si un autre thread a demandé le même verrou, la JVM transmet le verrou à ce thread.

Les verrous de classe sont en fait implémentés en tant que verrous d'objet. Lorsque la machine virtuelle Java charge un fichier de classe, elle crée une instance de classe java.lang.Class. Lorsque vous verrouillez une classe, vous verrouillez en fait l' Classobjet de cette classe .

Les threads n'ont pas besoin d'obtenir un verrou pour accéder aux variables d'instance ou de classe. Si un thread obtient un verrou, cependant, aucun autre thread ne peut accéder aux données verrouillées jusqu'à ce que le thread qui possède le verrou le libère.

Moniteurs

La machine virtuelle Java utilise des verrous avec des moniteurs . Un moniteur est fondamentalement un gardien en ce sens qu'il surveille une séquence de code, s'assurant qu'un seul thread à la fois exécute le code.

Chaque moniteur est associé à une référence d'objet. Lorsqu'un thread arrive à la première instruction dans un bloc de code placé sous l'œil vigilant d'un moniteur, le thread doit obtenir un verrou sur l'objet référencé. Le thread n'est pas autorisé à exécuter le code tant qu'il n'a pas obtenu le verrou. Une fois qu'il a obtenu le verrou, le thread entre dans le bloc de code protégé.

Lorsque le thread quitte le bloc, peu importe comment il quitte le bloc, il libère le verrou sur l'objet associé.

Serrures multiples

Un seul thread est autorisé à verrouiller le même objet plusieurs fois. Pour chaque objet, la JVM tient à jour un décompte du nombre de fois où l'objet a été verrouillé. Un objet déverrouillé a un compte de zéro. Lorsqu'un thread acquiert le verrou pour la première fois, le nombre est incrémenté à un. Chaque fois que le thread acquiert un verrou sur le même objet, un décompte est incrémenté. Chaque fois que le thread libère le verrou, le décompte est décrémenté. Lorsque le nombre atteint zéro, le verrou est libéré et mis à la disposition d'autres threads.

Blocs synchronisés

Dans la terminologie du langage Java, la coordination de plusieurs threads qui doivent accéder aux données partagées est appelée synchronisation . Le langage fournit deux méthodes intégrées pour synchroniser l'accès aux données: avec des instructions synchronisées ou des méthodes synchronisées.

Instructions synchronisées

Pour créer une instruction synchronisée, vous utilisez le synchronizedmot - clé avec une expression qui évalue une référence d'objet, comme dans la reverseOrder()méthode ci-dessous:

class KitchenSync { private int[] intArray = new int[10]; void reverseOrder() { synchronized (this) { int halfWay = intArray.length / 2; for (int i = 0; i < halfWay; ++i) { int upperIndex = intArray.length - 1 - i; int save = intArray[upperIndex]; intArray[upperIndex] = intArray[i]; intArray[i] = save; } } } }

Dans le cas ci-dessus, les instructions contenues dans le bloc synchronisé ne seront pas exécutées tant qu'un verrou n'est pas acquis sur l'objet courant ( this). Si, au lieu d'une thisréférence, l'expression produisait une référence à un autre objet, le verrou associé à cet objet serait acquis avant la poursuite du thread.

Deux opcodes, monitorenteret monitorexit, sont utilisés pour les blocs de synchronisation dans les méthodes, comme indiqué dans le tableau ci-dessous.

Tableau 1. Moniteurs

Opcode Opérande (s) La description
monitorenter aucun pop objectref, acquérez le verrou associé à objectref
monitorexit aucun pop objectref, relâchez le verrou associé à objectref

Lorsqu'il monitorenterest rencontré par la machine virtuelle Java, elle acquiert le verrou de l'objet référencé par objectref sur la pile. Si le thread possède déjà le verrou pour cet objet, un décompte est incrémenté. Chaque fois monitorexitque le thread est exécuté sur l'objet, le décompte est décrémenté. Lorsque le compte atteint zéro, le moniteur est libéré.

Jetez un œil à la séquence de bytecode générée par la reverseOrder()méthode de la KitchenSyncclasse.

Note that a catch clause ensures the locked object will be unlocked even if an exception is thrown from within the synchronized block. No matter how the synchronized block is exited, the object lock acquired when the thread entered the block definitely will be released.

Synchronized methods

To synchronize an entire method, you just include the synchronized keyword as one of the method qualifiers, as in:

class HeatSync { private int[] intArray = new int[10]; synchronized void reverseOrder() { int halfWay = intArray.length / 2; for (int i = 0; i < halfWay; ++i) { int upperIndex = intArray.length - 1 - i; int save = intArray[upperIndex]; intArray[upperIndex] = intArray[i]; intArray[i] = save; } } }

The JVM does not use any special opcodes to invoke or return from synchronized methods. When the JVM resolves the symbolic reference to a method, it determines whether the method is synchronized. If it is, the JVM acquires a lock before invoking the method. For an instance method, the JVM acquires the lock associated with the object upon which the method is being invoked. For a class method, it acquires the lock associated with the class to which the method belongs. After a synchronized method completes, whether it completes by returning or by throwing an exception, the lock is released.

Coming next month

Now that I have gone through the entire bytecode instruction set, I will be broadening the scope of this column to include various aspects or applications of Java technology, not just the Java virtual machine. Next month, I'll begin a multi-part series that gives an in-depth overview of Java's security model.

Bill Venners écrit des logiciels de manière professionnelle depuis 12 ans. Basé dans la Silicon Valley, il fournit des services de conseil et de formation en logiciels sous le nom d'Artima Software Company. Au fil des ans, il a développé des logiciels pour les secteurs de l'électronique grand public, de l'éducation, des semi-conducteurs et de l'assurance-vie. Il a programmé dans de nombreux langages sur de nombreuses plateformes: langage d'assemblage sur différents microprocesseurs, C sous Unix, C ++ sous Windows, Java sur le Web. Il est l'auteur du livre: Inside the Java Virtual Machine, publié par McGraw-Hill.

En savoir plus sur ce sujet

  • The book The Java virtual machine Specification (//www.aw.com/cp/lindholm-yellin.html), by Tim Lindholm and Frank Yellin (ISBN 0-201-63452-X), part of The Java Series (//www.aw.com/cp/javaseries.html), from Addison-Wesley, is the definitive Java virtual machine reference.
  • Previous "Under The Hood" articles:
  • "The Lean, Mean Virtual Machine" Gives an introduction to the Java virtual machine.
  • "The Java Class File Lifestyle" Gives an overview to the Java class file, the file format into which all Java programs are compiled.
  • "Java's Garbage-Collected Heap" Gives an overview of garbage collection in general and the garbage-collected heap of the Java virtual machine in particular.
  • "Bytecode Basics" Introduces the bytecodes of the Java virtual machine, and discusses primitive types, conversion operations, and stack operations in particular.
  • "Floating Point Arithmetic" Describes the Java virtual machine's floating-point support and the bytecodes that perform floating point operations.
  • "Logic and Arithmetic" Describes the Java virtual machine's support for logical and integer arithmetic, and the related bytecodes.
  • "Objects and Arrays" Describes how the Java virtual machine deals with objects and arrays, and discusses the relevant bytecodes.
  • "Exceptions" Describes how the Java virtual machine deals with exceptions, and discusses the relevant bytecodes.
  • "Try-finally" Décrit comment la machine virtuelle Java implémente les clauses try-finally et discute des bytecodes appropriés.
  • "Flux de contrôle" Décrit comment la machine virtuelle Java implémente le flux de contrôle et décrit les bytecodes pertinents.
  • "L'architecture des Aglets" Décrit le fonctionnement interne des Aglets, la technologie d'agent logiciel autonome basée sur Java d'IBM.
  • "The Point of Aglets" Analyse l'utilité réelle des agents mobiles tels que Aglets, la technologie d'agent logiciel autonome basée sur Java d'IBM.
  • «Invocation et retour de méthode» Explique comment la machine virtuelle Java appelle et renvoie des méthodes, y compris les bytecodes appropriés.

Cette histoire, "Comment la machine virtuelle Java effectue la synchronisation des threads" a été publiée à l'origine par JavaWorld.