16.3 Monitors and Object Locks

Each object has a monitor associated with it. The JVM guarantees that a monitor may be owned by at most one thread. This provides JVM programmers with the ability to implement many different kinds of synchronization control.

The JVM has two instructions for operating on monitors: monitorenter and monitorexit. When a thread issues the monitorenter instruction, it acquires the monitor of the object on top of the stack. If some other thread owns the monitor for that object, the thread requesting the object will block until the monitor is released. The monitor is released when the monitorexit instruction is performed on the same object.

Because only one thread may own a monitor at a time, programs may use the monitor to synchronize between threads. One use of this synchronization is for locks. If the threads agree that they must acquire the monitor on some object before performing an operation, then that object acts like a lock: only one thread may perform that operation at a time.

For example, suppose that you have a number of threads running. Each thread writes diagnostic messages to the console. A thread should be able to write an entire message to the console before another thread begins to write its message. If two threads tried to write messages to the console at the same time, the console would be unreadable.

The solution is to use monitors to control access to the System.out object so that only one thread may use it at a time:

 getfield java/lang/System/out    Ljava/io/PrintStream;                     ; Push System.out monitorenter                                 ; Lock it ; Only one thread at a time can be in this code, since the ; monitorenter will block until no other thread is using the ; System.out object. ; This is called the critical section ;; Insert code here which dumps a message to System.out getfield java/lang/System/out    Ljava/io/PrintStream;                     ; Push System.out monitorexit                                  ; Release it 

Because of the monitorenter instruction, at most one thread may be in the critical section at a time. The critical section prints out the entire message before executing the monitorexit instruction, ensuring that messages do not overlap.

If several threads are waiting for the monitor at the same time, only one gets it. The others continue to wait. Different virtual machine implementations have different techniques for choosing which thread will get control of the monitor next. Generally, implementations will try to distribute control of the monitor fairly among the threads.

It is important to note that this is purely an advisory lock. Programs that use monitorenter obtain the lock before using the object. The JVM doesn't enforce that restriction; it must be written explicitly by the programmer. The part the JVM plays is to ensure that only one thread owns the monitor at a time.

A thread can keep a monitor lock as long as it wants. It may even exit the method containing the monitorenter instruction. This is a simple implementation of semaphores using the monitor instructions:

 .class sem ; P: gain control of semaphore .method p()V aload_0 monitorenter          ; Leave method without exiting monitor return .end method ; V: release semaphore .method V()V aload_0 monitorexit           ; Now release the monitor return .end method 

This use of the monitor instructions is strongly discouraged, since it is very easy for the programmer using methods like these to forget to release a monitor that is held. This can easily lead to deadlock, in which two threads wait forever for the other one to surrender a monitor. Instead, it is better to have a monitorexit for each monitorenter instruction within a single method. This is the approach used by Java compilers (see section 16.3.1 for more details on how it does this).

A thread may obtain the monitor more than once. In order to release the monitor, it must use monitorexit exactly as many times as it calls monitorenter on the object. For example:

 .class MyClass .method foo(Ljava/lang/Object;)V    aload_1                ; Push the object to lock    monitorenter           ; Lock it. The lock count is 1    aload_0                ; Call bar with the lock object    aload_1    invokevirtual MyClass/bar (Ljava/lang/Object;)V    aload_1                ; Release the lock. Now the lock    monitorexit            ; count is 0, and others may get it .end method ; This method is called from foo .method bar(Ljava/lang/Object;)V    aload_1                ; Lock the object again. The lock    monitorenter           ; count is now 2    ;; Do some stuff    aload_1                ; Release the object. The lock count    monitorexit            ; is 1    return .end method 

Once a thread has the monitor, additional locks on the monitor will happen almost instantaneously. There is no need for the thread to compete for ownership of the monitor.

Each monitorenter increases the lock count by 1, and each monitorexit decreases it by 1. Only when the lock count reaches 0 may other threads have the chance to get it. The race for the monitor happens immediately after the lock count reaches 0. For example:

 aload_0                  ; Lock the object monitorenter aload_0                  ; Push the lock object twice dup monitorexit              ; Lock it once, then try to get monitorenter             ; it again immediately 

Even though the monitorenter occurs immediately after the monitorexit, there is no guarantee that the same thread will get the lock again immediately. It has to wait its turn with all the other threads waiting for the monitor on this object.

16.3.1 Synchronization in Java

In Java, monitorenter and monitorexit instructions are generated in pairs by a synchronized statement. A synchronized statement looks like this:

 synchronized(obj) {    // In here, the current thread has exclusive access to obj } 

When this is compiled, the synchronized block is surrounded by monitorenter and monitorexit instructions. To ensure that the monitor is always released, an exception handler is used to catch all exceptions. The code compiles to

 .catch all from begin to end using handler    aload_1                ; Push the Object    monitorenter           ; Lock it begin:    ;; The body of the synchronized statement goes here end:    aload_1                             ; Release the lock    monitorexit    return ; If any exceptions occur during the synchronized statement, ; this exception handler will be executed. It ensures that ; the lock is released handler:    aload_1                             ; Release the lock    monitorexit    return 

16.3.2 Synchronized Methods

We said before that the monitorenter and monitorexit instructions are advisory locks. There is no way to require that a monitorenter be executed before using an object. It is up to the programmer using an object to know whether an object should be locked before use. Any thread failing to obtain a lock before using an object may corrupt the data in that object. Consider, for example, a shared log file:

 .class LogFile .field static theLog LLogFile;           ; The main log file .method printMessage(Ljava/lang/String;)V ;; Print the message .end method 

To log a message, it is expected that the thread lock the log object before use:

 getstatic Logfile/theLog LLlogFile;      ; Get the log file monitorenter                             ; Lock it getstatic Logfile/theLog LLlogFile;      ; Get the log file ldc "Some log string" invokevirtual LogFile/printMessage    (Ljava/lang/String;)V                 ; Print a message getstatic Logfile/theLog LLlogFile;      ; Release the lock monitorexit 

If the monitorenter or monitorexit instructions are forgotten, or if the program has a way to execute one without executing the other, there will be problems. A monitorenter without a monitorexit will cause the log to be held indefinitely. If the program issues a monitorexit without having already obtained the lock, the JVM will cause an IllegalMonitorStateException to be thrown.

For this reason, the JVM provides a way to force a program to acquire the monitor on an object before invoking a method on the object. By declaring a method synchronized, any thread attempting to use that method obtains a lock on the object first. To ensure that the log must be locked before the printMessage method can be called, declare it like this:

 .method synchronized printMessage(Ljava/lang/String;)V 

When this method is invoked, the JVM automatically acquires the monitor for the object on which the method is invoked. If the monitor is currently owned by some other thread, then the current thread blocks until the monitor becomes free.

When the method ends, whether normally through a return instruction or abnormally because of an exception being thrown, the monitor is released automatically. This guarantees that the thread cannot accidentally keep control of the monitor after leaving the method.

Only one thread at a time may use the LogFile, even without explicit monitorenter instructions. Users of synchronized methods are relieved of the responsibility of explicitly obtaining the locks themselves.

It is important to note that the lock lasts only as long as the call to printMessage. If it were necessary to print two messages in a row without any intervening messages, it would be necessary to lock the object in the calling code:

 aload_1               ; Push the log monitorenter          ; And lock it ;; Invoke printMessage for the first message ;; Invoke printMessage for the second message aload_1               ; Release the log monitorexit 

A lock on the log is obtained three times by this thread: once by the monitorenter instruction and twice by the two calls to printMessage. During the interval between calls to printMessage, the current thread still has the lock, and nobody else can call printMessage since the call is synchronized. Both the monitorenter and synchronized method calls obtain the same lock.

That means that while in printMessage, the current thread has the same lock twice. This does not present a problem, since a thread can reobtain a lock it already has without waiting. The lock count increases to 2. When the call to printMessage ends, the lock count drops to 1, which means that the thread continues to hold the monitor.

A synchronized method can call another synchronized method on the same object. It can even call itself, recursively. Since it already has the lock on the object, it increases the lock count. Each time the method returns, the lock count decreases. When the topmost call returns, the lock count drops to 0, and other threads can try to obtain the lock.

You can synchronize static methods, too. In that case, the object that is locked is the Class object corresponding to the class. For example, suppose you need a class Counter, which maintains a one-up counter to be used by several threads. It's necessary to synchronize access to the counter value. A possible definition of the class:

 .class Counter .field static private next I   ; The next value of the counter ; A method to increment and return the counter .method synchronized static getNextCounter ()I .limit stack 2 getstatic Counter/next I       ; Get the field istore_0                       ; Store it in a local value iload_0                        ; Increment the value iconst_1 iadd putstatic Counter/next I       ; Store the increment value iload_0                        ; Return the original value ireturn .end method 

Within the body of getNextCounter, the thread obtains a lock on the object returned by Class.forName("Counter") (or Counter.class, in the notation of Java 1.1). This object is unique within the JVM; that is, any thread requesting Counter.class receives a reference to the same object. This means that all threads attempting to synchronize on the class are trying to synchronize on the same object.



Programming for the Java Virtual Machine
Programming for the Javaв„ў Virtual Machine
ISBN: 0201309726
EAN: 2147483647
Year: 1998
Pages: 158
Authors: Joshua Engel

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net