Come la Java virtual machine esegue la sincronizzazione dei thread

Tutti i programmi Java sono compilati in file di classe, che contengono bytecode, il linguaggio macchina della macchina virtuale Java. Questo articolo esamina come la sincronizzazione dei thread viene gestita dalla Java virtual machine, inclusi i bytecode pertinenti. (1.750 parole)

Under The Hood di questo mese esamina la sincronizzazione dei thread sia nel linguaggio Java che nella Java virtual machine (JVM). Questo articolo è l'ultimo della lunga serie di articoli di bytecode che ho iniziato la scorsa estate. Descrive gli unici due codici operativi direttamente correlati alla sincronizzazione dei thread, i codici operativi utilizzati per entrare e uscire dai monitor.

Thread e dati condivisi

Uno dei punti di forza del linguaggio di programmazione Java è il supporto per il multithreading a livello di linguaggio. Gran parte di questo supporto è incentrato sul coordinamento dell'accesso ai dati condivisi tra più thread.

La JVM organizza i dati di un'applicazione Java in esecuzione in diverse aree di dati di runtime: uno o più stack Java, un heap e un'area del metodo. Per un approfondimento su queste aree di memoria, vedere il primo articolo Dietro le quinte : "La macchina virtuale snella e media".

All'interno della Java virtual machine, a ogni thread viene assegnato uno stack Java , che contiene dati a cui nessun altro thread può accedere, comprese le variabili locali, i parametri e i valori di ritorno di ogni metodo richiamato dal thread. I dati nello stack sono limitati ai tipi primitivi e ai riferimenti agli oggetti. Nella JVM, non è possibile posizionare l'immagine di un oggetto reale sullo stack. Tutti gli oggetti risiedono nell'heap.

C'è un solo heap all'interno della JVM e tutti i thread lo condividono. L'heap non contiene altro che oggetti. Non c'è modo di inserire un tipo primitivo solitario o un riferimento a un oggetto nell'heap: queste cose devono essere parte di un oggetto. Gli array risiedono nell'heap, inclusi gli array di tipi primitivi, ma in Java anche gli array sono oggetti.

Oltre allo stack Java e all'heap, l'altro punto in cui i dati possono risiedere nella JVM è l' area del metodo , che contiene tutte le variabili di classe (o statiche) utilizzate dal programma. L'area del metodo è simile allo stack in quanto contiene solo tipi primitivi e riferimenti a oggetti. A differenza dello stack, tuttavia, le variabili di classe nell'area del metodo sono condivise da tutti i thread.

Blocchi di oggetti e classi

Come descritto sopra, due aree di memoria nella Java virtual machine contengono dati condivisi da tutti i thread. Questi sono:

  • L'heap, che contiene tutti gli oggetti
  • L'area del metodo, che contiene tutte le variabili di classe

Se più thread devono utilizzare gli stessi oggetti o variabili di classe contemporaneamente, il loro accesso ai dati deve essere gestito correttamente. In caso contrario, il programma avrà un comportamento imprevedibile.

Per coordinare l'accesso ai dati condivisi tra più thread, la Java virtual machine associa un blocco a ogni oggetto e classe. Un blocco è come un privilegio che solo un thread può "possedere" alla volta. Se un thread vuole bloccare un particolare oggetto o classe, chiede alla JVM. Ad un certo punto dopo che il thread ha chiesto un blocco alla JVM - forse molto presto, forse più tardi, forse mai - la JVM dà il blocco al thread. Quando il thread non ha più bisogno del blocco, lo restituisce alla JVM. Se un altro thread ha richiesto lo stesso blocco, la JVM passa il blocco a quel thread.

I blocchi di classe vengono effettivamente implementati come blocchi di oggetti. Quando la JVM carica un file di classe, crea un'istanza di classe java.lang.Class. Quando blocchi una classe, stai effettivamente bloccando l' Classoggetto di quella classe .

Non è necessario che i thread ottengano un blocco per accedere alle variabili di istanza o di classe. Se un thread ottiene un blocco, tuttavia, nessun altro thread può accedere ai dati bloccati finché il thread che possiede il blocco non lo rilascia.

Monitor

La JVM utilizza i blocchi insieme ai monitor . Un monitor è fondamentalmente un guardiano in quanto sorveglia una sequenza di codice, assicurandosi che solo un thread alla volta esegua il codice.

Ogni monitor è associato a un riferimento a un oggetto. Quando un thread arriva alla prima istruzione in un blocco di codice che è sotto l'occhio vigile di un monitor, il thread deve ottenere un blocco sull'oggetto di riferimento. Al thread non è consentito eseguire il codice finché non ottiene il blocco. Una volta ottenuto il blocco, il thread entra nel blocco di codice protetto.

Quando il thread lascia il blocco, indipendentemente da come lascia il blocco, rilascia il blocco sull'oggetto associato.

Serrature multiple

Un singolo thread può bloccare lo stesso oggetto più volte. Per ogni oggetto, la JVM mantiene un conteggio del numero di volte in cui l'oggetto è stato bloccato. Un oggetto sbloccato ha un conteggio pari a zero. Quando un thread acquisisce il blocco per la prima volta, il conteggio viene incrementato a uno. Ogni volta che il thread acquisisce un blocco sullo stesso oggetto, viene incrementato un conteggio. Ogni volta che il thread rilascia il blocco, il conteggio viene decrementato. Quando il conteggio raggiunge lo zero, il blocco viene rilasciato e reso disponibile ad altri thread.

Blocchi sincronizzati

Nella terminologia del linguaggio Java, il coordinamento di più thread che devono accedere ai dati condivisi è chiamato sincronizzazione . Il linguaggio fornisce due modalità integrate per sincronizzare l'accesso ai dati: con istruzioni sincronizzate o metodi sincronizzati.

Dichiarazioni sincronizzate

Per creare un'istruzione sincronizzata, utilizzare la synchronizedparola chiave con un'espressione che restituisce un riferimento a un oggetto, come nel reverseOrder()metodo seguente:

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

Nel caso precedente, le istruzioni contenute nel blocco sincronizzato non verranno eseguite finché non viene acquisito un blocco sull'oggetto corrente ( this). Se invece di un thisriferimento, l'espressione restituisse un riferimento a un altro oggetto, il blocco associato a quell'oggetto verrebbe acquisito prima che il thread continuasse.

Due codici operativi, monitorentere monitorexit, vengono utilizzati per i blocchi di sincronizzazione all'interno dei metodi, come mostrato nella tabella seguente.

Tabella 1. Monitor

Codice operativo Operando (i) Descrizione
monitorenter nessuna pop objectref, acquisisce il blocco associato a objectref
monitorexit nessuna pop objectref, rilascia il blocco associato a objectref

Quando monitorenterviene rilevato dalla Java virtual machine, acquisisce il blocco per l'oggetto a cui fa riferimento objectref sullo stack. Se il thread possiede già il blocco per quell'oggetto, viene incrementato un conteggio. Ogni volta che monitorexitviene eseguito per il thread sull'oggetto, il conteggio viene decrementato. Quando il conteggio raggiunge lo zero, il monitor viene rilasciato.

Dai un'occhiata alla sequenza del bytecode generata dal reverseOrder()metodo della 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 scrive software professionalmente da 12 anni. Con sede nella Silicon Valley, fornisce consulenza sul software e servizi di formazione sotto il nome di Artima Software Company. Negli anni ha sviluppato software per i settori dell'elettronica di consumo, dell'istruzione, dei semiconduttori e delle assicurazioni sulla vita. Ha programmato in molti linguaggi su molte piattaforme: linguaggio assembly su vari microprocessori, C su Unix, C ++ su Windows, Java sul Web. È autore del libro: Inside the Java Virtual Machine, pubblicato da McGraw-Hill.

Ulteriori informazioni su questo argomento

  • 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-Finalmente" Descrive come la Java virtual machine implementa le clausole try-finalmente e discute i bytecode pertinenti.
  • "Flusso di controllo" Descrive come la Java virtual machine implementa il flusso di controllo e discute i bytecode pertinenti.
  • "The Architecture of Aglet" Descrive il funzionamento interno di Aglet, la tecnologia agent software autonoma basata su Java di IBM.
  • "The Point of Aglets" Analizza l'utilità del mondo reale degli agenti mobili come gli Aglet, la tecnologia dell'agente software autonomo basato su Java di IBM.
  • "Invocazione e restituzione del metodo" Spiega come la Java virtual machine richiama e ritorna dai metodi, inclusi i bytecode pertinenti.

Questa storia, "Come la macchina virtuale Java esegue la sincronizzazione dei thread" è stata originariamente pubblicata da JavaWorld.