Back to Article

Java Memory Model

by Benoy Jose

Introduction

The Java Language specification has a whole chapter dedicated to explaining how threads and the Java memory model works, but the implementation details of the memory model are minimal and cause a lot of confusion. The Java specification only defines rules on how threads should work and how memory should be managed while the details were left out to individual implementations of the JVM. Since the rules were not well detailed they leave a lot of flexibility to the JVM implementers on how they manage memory and handle threads. As a result of this we have JVM implementations that adhere to all the rules in the Java specification while they add their own optimizations to the JVM to make it run faster. So memory management and thread management are confusing to the programmers. The JSR 133 tries to explain the memory model in detail and show how the JVM deals with threads and the memory. The specification attempts to explain how incorrectly designed applications can behave in erroneous ways because the JVM implementation might optimize the program to run faster.

Memory Model

A memory model defines the possible scenarios and rules that govern multiple threads in a system. A memory model determines if an execution trace of a program is legally allowed by the JVM. The Java specification does not force JVM implementations to follow any particular implementation rules for program execution; this gives the JVM implementer flexibility to provide compiler optimizations and reorganizations in the execution order. However the memory model specifies that all implementations produce results that can be predicted by a programmer. The Memory model defines the possible rules for threads and the expected behavior of multi-threaded programs so that programmers can design their programs accordingly. The responsibility of avoiding data races and deadlock conditions in threads still lies with the implementer and the programmer.

Features

Synchronization

The Java Memory model explains synchronization techniques to make sure data corruption does not take place. Synchronization can also help to avoid deadlocks between threads and run programs smoothly. The next section details a condition called Data Race which will illustrate the advantages of synchronization further.

Lock

The Java memory model provides for locks on monitors when synchronization needs to be done. Any thread that needs to execute a synchronized block in a class, first acquires a lock on the monitor of that object. When the synchronized code block has finished executing the lock is released and the object is accessible to another thread.

Atomic Reads

The Java language specification gives compilers the flexibility on how operations on 64 bit values should be read and operated upon. Some implementations and compilers may find it easy to divide a 64 bit double and long values into two 32 bit and write them to adjacent locations. The Java memory model treats the two 32 bit writes as separate writes, so there is a possibility of a thread seeing a partial value when the virtual machine is mid-way during a write. To avoid the problem the variable can be declared as volatile or a reference to the variable can be used to do operation. Volatile long and volatile double values are always read as a single atomic unit. It is the same case with references, irrespective of whether they are declared as 32 bit or 64 bit values.

Word Tearing

Word Tearing occurs when some processors do not allow for a single byte to be updated individually. In old processors writes to memory are done through a word (two bytes), so in these cases the processor would read the whole word from memory and update the appropriate byte and then write the word back into memory. This is called word tearing. In modern processors this problem does not exist as they allow a single byte to be written to memory.

Fairness

The Java memory model does not specify any fairness requirement for threads or preemptive multi-threading. A thread can refuse to surrender the CPU to another thread and throw the system into deadlock. The rules for fairness to other threads are defined by the individual JVM implementations.

Wait Sets

A wait set contains a set of threads for a particular object. Each object has a wait set that is empty when the object is first created. Every thread, which invokes a wait command, is put into the wait set. The wait could be a timed wait or an indefinite wait.

Notification

Notification occurs when the method notify() or notifyAll() is executed. The call to notify () will release one arbitrary thread inside the wait set. The notifyAll releases all the threads waiting inside a wait set.

Sleep and Yield

Thread.sleep() causes the current thread to sleep for the specified milliseconds. Both yield and sleep do not lose the ownership on the monitor they have acquired. The compilers need not flush out the values in the registers and reload cached values from memory during the sleep and yield process.

Finalization

The object class which is the super class of all classes has a finalize method which is used to do any cleanup operations before the object’s memory is reclaimed by the garbage collector. The cleanup is important because orphaned references remain in memory when an object’s memory is reclaimed. The Java memory model does not define strict rules on when finalization will be done by the garbage collector. It is left to JVM implementations on when finalization should be done. However the memory model specifies that finalization should be surely done before a object’s memory is reclaimed.

Volatile Fields

Declaring a variable as volatile is almost similar to having a synchronized variable. The only difference is that no locking or unlocking needs to be done. Volatile fields can be useful when manipulations on a few variables are required and synchronization can be an overhead. But if too many variables are declared as volatile in a method they degrade the performance of the method. In such cases it would be sensible to declare the method as synchronized.

Data Races

The Java language specification gives compilers flexibility to order program execution for optimization purposes. This flexibility can result in programs being ordered differently from what the programmer would have expected. Let us look at an example.

public class SynchSample{
private int classVar1 = 0;
private int classVar2=0;
private int outputVar1;
private int outputVar2;

public SysnchSample(){
}

public void doOperation1(){
	outputVar1= classVar1;
	classVar2 = 4;
}

public void doOperation2(){
	outputVar2= classVar2;
	classVar1=5;
}	
}

Listing 1:

Running this program in a single threaded environment where doOperation1 and doOperation2 are called one after another would yield outputVar1=0 and outputVar2= 4. This seems fine, now if you try to run this program in a multi-threaded environment the results could be different.

Let us create two threads thread 1 and thread 2, which would access the same instance (SynchSample object). Thread 1 would run doOperation1 and thread 2 will run doOperation2. In this case there is a possibility that either thread 1 or thread 2 would get executed first. In the case when thread 1 is executed first one would assume that the value of outputVar1 would remain as 0, but when outputVar1 is displayed it shows a value of 5. This is an anomaly that programmers do not expect. Though classVar1 was assigned the value 5 later when the thread 2 executes, the value gets reflected into the outputVar1. This seems paradoxical but it is the actual behavior exhibited by the Java memory model. This condition is called a "Data Race".

To avoid this condition of a data race, programs should be correctly synchronized. The process of synchronization does not guarantee the proper running of the program but it allows the programmer to predict the behavior of the program and avoid undesirable results.

Synchronization can be achieved by defining a happens- before relationship to actions. When a happens-before relation is defined for an action, it gets executed before the subsequent actions and is visible to the subsequent actions. Though there is no specific construct or command in the Java language to define a happens-before relationship, it can be done in many ways like ordering an action before another in a program and surrounding actions with the synchronized keyword. The synchronized keyword should be used carefully to avoid unnecessary overheads on the performance of the program.

    Other ways of implementing the happens-before relationship are:
  • Using the volatile keyword, which always does a write to the field before a read can be done.
  • Defining actions in the start method of a thread as the start method is always called before executing any other actions on the class.
  • Using join, every action in a thread is usually executed before other threads successfully return from a join from that thread.

Surprises in the Memory Model

The sample shown in listing 1 was one of the surprises of memory model. There are a few more, which are interesting. The Java language specification assures us that strings are immutable, so most operations on a string destroy the original object and create a new one. But in reality the compiler can optimize the operation and reuse the same memory as the original string. For instance we have a string ‘String stringSample = "javaboutique";’ stored in memory at memory location m1. The compiler will store this String in memory as origin location = m1 and offset = 12. Now when an operation ‘stringSample = stringSample.substring(4);’ is done, stringSample will contain "boutique". As per the Java specification we would assume that stringSample will be moved to a new location m2 and the old memory location is left for the garbage collector, but compilers can reuse the same memory location and update the offsets. So the new string after the sub string operation would read as origin location = m1+4 and offset = 8. This does not break the Java specification, as the old reference is lost and a new reference is created with a new origin location and a new offset. The problem is when two threads try to access this string at the same time; the second thread might get the wrong starting location. Synchronizing these methods would alleviate the problem.

Final Fields

Java provides for final fields, which are initialized once and are never changed for the life of the object instance. Unlike normal fields, compilers can cache final fields into registers and read from there without reloading them from memory again. In the case of normal fields values can be cached into registers but need to be synchronized with the value in memory when a read is done.

Final fields are of great value in multi-threaded environments as programmers can use them as thread safe immutable objects, which do not need synchronization. Since an immutable field cannot be modified there is a guarantee that the field will not behave erratically even in a data race condition.

Final fields can be initialized either directly in the class when they are declared or through the constructor of the class. According to the Java specification an object is deemed as completely initialized only when the constructor of the class has completed execution. Since the final variables are initialized in the constructor there is a guarantee that the final field is initialized and available to threads with the correct value. The Java memory model ensures that threads get access to final fields only after the object is completely initialized. The Java memory model ensures this by freezing the final field when the constructor for the class exits normally. In the case of constructors calling their super classes final field freeze takes place when the last invoked constructor exits normally.

A final field is considered unchangeable after it is first initialized to a value, but there could be situations when a final field needs to be modified after it is created. One of the situations is during deserialization (reverse of serialization). During deserialization the system will need to change the value of a final field after it has been initialized and frozen. During deserialization the object is created first and then the field values are set. In this case the freeze on a final field occurs only after the constructor has exited and the final fields have been set. So the object should be made unavailable to all threads till the final fields have been set into the object.

Conclusion

The JSR 133 (Java Memory model and thread specification) is not intended as enhancement JSR or a new feature. The memory Model spec should be seen as an addendum to the original Java language specification. The specification does not introduce any new concepts or provide new functionality but serves more like a clarification to the original Java specification. Though a good effort has been made to clarify how threads work and how memory operates, the specification still leaves some questions unanswered. Most of the unanswered questions are centered on how compilers make optimizations to programs and order instructions within a program and how these changes affect the memory. The individual JVM implementers best answer these questions, as they are the ones who use these optimizations within the JVM. Though the specification does not clearly state how the threads are handled in individual JVM implementations, it provides details on what a programmer can expect when dealing with threads.

Benoy Jose is a web developer with over six years of experience in J2EE and Microsoft technologies. He is a Sun Certified programmer and enjoys writing technical and non-technical articles for various magazines.