Java Memory Model

The Java Memory Model (JMM) describes the behavior of threads in the Java runtime. It is part of the semantics of the Java language, a set of rules that describe the execution of multithreaded programs and rules by which threads can communicate with each other through main memory.

Formally, the memory model defines a set of inter-thread communication actions (these actions include, in particular, reading and writing a variable, capturing and releasing a monitor, reading and writing a volatile variable, starting a new thread), and also the memory model defines the relationship between these actions -happens-before - an abstraction indicating that if operation X is related by happens-before to operation Y, then all code following operation Y executed in one thread sees all changes made by another thread before operation X.

There are several basic rules for happens-before relationships:

  • Within one thread, any operation happens-before any operation following it in the source code;
  • Freeing a monitor (unlock) happens-before capturing the same monitor (lock);
  • Exiting a synchronized block/method happens-before entering a synchronized block/method on the same monitor;
  • Writing a volatile field happens-before reading the same volatile field;
  • The run() method of an instance of Thread happens-before exiting the join() method, or returning false by the isAlive() method by an instance of the same thread;
  • Calling the start() method of an instance of Thread happens-before the run() method of an instance of the same thread starts;
  • Constructor completion happens-before the start of the finalize() method of this class;
  • Calling the interrupt() method on a thread happens-before the thread detects that the method was called by either throwing an InterruptedException exception, or using the isInterrupted() or interrupted() methods.
  • The happens-before relationship is transitive, i.e. if X happens-before Y and Y happens-before Z, then X happens-before Z.
  • Freeing/capturing the monitor and writing/reading to a volatile variable are connected by a happens-before relationship only if operations are performed on the same object instance.
  • In a happens-before relationship, only two threads are involved, and nothing can be said about the behavior of the other threads until each of them has a happens-before relationship with another thread.

There are several main areas related to the memory model:

Visibility. One thread may, at some point, temporarily save the value of some fields not to main memory, but to registers or the local processor cache, so a second thread running on another processor, reading from main memory, may not see the last field changes. Conversely, if a thread has been working with registers and local caches for some time, reading data from there, it may not immediately see the changes made by another thread to the main memory.

The following Java keywords are relevant to the issue of visibility: synchronized, volatile, final.

From a Java perspective, all variables (with the exception of local variables declared inside a method) are stored in main memory, which is accessible to all threads, except that each thread has a local — working — memory where it stores copies of the variables with which it operates, and when executing the program, the thread only works with these copies. It should be noted that this description is not an implementation requirement, but just a model that explains the behavior of the program, for example, the cache memory does not necessarily act as local memory, it may be processor registers or threads may not have local memory at all.

When entering a synchronized method or block, the thread updates the contents of local memory, and when exiting a synchronized method or block, the thread writes the changes made in local memory to the main memory. This behavior of synchronized methods and blocks follows from the rules for the "happens before" relationship: since all memory operations occur before the monitor is released and the monitor is released before the monitor is captured, then all memory operations that were done by the thread before exiting the synchronized block must be visible to any thread that enters a synchronized block for the same monitor. It is very important that this rule only works if the threads are synchronized using the same monitor!

As for volatile variables, such variables are written to main memory, bypassing local memory and reading of a volatile variable is also performed from main memory, that is, the value of the variable cannot be stored in registers or the local memory of the thread, and the operation of reading this variable is guaranteed to return the last value written to it.

Also, the memory model defines additional semantics of the final keyword related to visibility: after an object has been correctly created, any thread can see the values of its final fields without additional synchronization. "Correctly created" means that a reference to the object being created should not be used until after the object's constructor has completed. The presence of such semantics for the final keyword allows the creation of immutable objects containing only final fields, such objects can be freely transferred between threads without ensuring synchronization during transfer.

There is one problem with final fields: the implementation allows the values of such fields to be changed after the object is created (this can be done, for example, using the reflection mechanism). If the final value of a field is a constant whose value is known at the time of compilation, changes to such a field may have no effect, since the calls to this variable could be replaced by a constant by the compiler. Also, the specification allows other optimizations related to final fields, for example, read operations on a final variable can be reordered with operations that can potentially change such a variable. So it is recommended to modify the final fields of an object only within the constructor, otherwise the behavior is unspecified.

Reordering. To increase performance, the processor/compiler can swap some instructions/operations. Rather, from the point of view of a thread observing the execution of operations in another thread, operations may not be performed in the order in which they occur in the source code. The same effect can be observed when one thread puts the results of the first operation in a register or local cache, and the result of the second operation goes directly to main memory. Then the second thread, referring to the main memory, can first see the result of the second operation, and only then the first, when all registers or caches are synchronized with the main memory. Another reason for reordering may be that the processor may decide to change the order of operations if, for example, it thinks that such a sequence is faster.

The reordering issue is also governed by a set of rules for the "comes before" relationship, and these rules have a practical ordering implication: reads and writes of volatile variables cannot be reordered with reads and writes of other volatile and non-volatile variables. This corollary makes it possible to use a volatile variable as a flag signaling the end of an action. Otherwise, the rules about the order of execution of operations ensure the ordering of operations for a specific set of cases (such as, for example, capturing and releasing a monitor), in all other cases leaving the compiler and the processor complete freedom to optimize.


Read also:


Comments

Popular posts from this blog

ArrayList and LinkedList in Java, memory usage and speed

XML, well-formed XML and valid XML

Methods for reading XML in Java