Skip to main content

Java Memory Model

Processors generally have one or more layers of memory cache (eg. L1 cache, L2 cache and L3 cache etc.) in multiprocessor systems which improves performance by speeding access to data (because the data is closer to the processor) and reducing traffic on the shared memory bus (because many memory operations can be satisfied by local caches.) Memory caches can improve performance tremendously, but they present a host of new challenges.
For example -
What would happens when two processors examine the same memory location at the same time?
Under what conditions will they see the same value?

A memory model defines necessary and sufficient conditions for knowing that writes to memory by other processors are visible to the current processor, and writes by the current processor are visible to other processors.

Some processors exhibit a strong memory model, where all processors see exactly the same value for any given memory location at all times. Other processors exhibit a weaker memory model, where special instructions, called memory barriers, are required to flush or invalidate the local processor cache in order to see writes made by other processors or make writes by this processor visible to others.

These memory barriers are usually performed when lock and unlock actions are taken; they are invisible to programmers in a high level language. It can sometimes be easier to write programs for strong memory models, because of the reduced need for memory barriers.

However, even on some of the strongest memory models, memory barriers are often necessary. Recent trends in processor design have encouraged weaker memory models because the relaxations they make for cache consistency allow for greater scalability across multiple processors and larger amounts of memory.

The issue of when a write becomes visible to another thread is compounded by the compiler's reordering of code. For example, the compiler might decide that it is more efficient to move a write operation later in the program; as long as this code motion does not change the program's semantics, it is free to do so.  If a compiler defers an operation, another thread will not see it until it is performed; this mirrors the effect of caching.

Moreover, writes to memory can be moved earlier in a program; in this case, other threads might see a write before it actually "occurs" in the program.  All of this flexibility is by design -- by giving the compiler, runtime, or hardware the flexibility to execute operations in the optimal order, within the bounds of the memory model, we can achieve higher performance. A simple example of this can be seen in the following code:

Class ROExample {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

Let's say that this code is executed in two threads concurrently

The read of y gives the value 2 if write happens before, the programmer might assume that the read of x must see the value 1 but it can only happen if the writes have been reordered brfore read. But if writes doesn't happen before read, r1 and r2 may have value 0.

The Java Memory Model describes what behaviors are legal in multi-threaded code, and how threads may interact through memory. It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system. It does this in a way that can be implemented correctly using a wide variety of hardware and a wide variety of compiler optimizations.


Java includes several language constructs, including volatile, final, and synchronized, which are intended to help the programmer describe a program's concurrency requirements to the compiler. The Java Memory Model defines the behavior of volatile and synchronized, and, more importantly, ensures that a correctly synchronized Java program runs correctly on all processor architectures.

Several serious flaws have been discovered in the Java Memory Model as defined in the Java Language Specification.

It was the first time that a programming language specification attempted to incorporate a memory model which could provide consistent semantics for concurrency across a variety of architectures. Unfortunately, defining a memory model which is both consistent and intuitive proved far more difficult than expected. JSR 133 defines a new memory model for the Java language which fixes the flaws of the earlier memory model. In order to do this, the semantics of final and volatile needed to change.

There are a number of cases in which accesses to program variables (object instance fields, class static fields, and array elements) may appear to execute in a different order than was specified by the program. The compiler is free to take liberties with the ordering of instructions in the name of optimization. Processors may execute instructions out of order under certain circumstances. Data may be moved between registers, processor caches, and main memory in different order than specified by the program. For example, if a thread writes to field a and then to field b, and the value of b does not depend on the value of a, then the compiler is free to reorder these operations, and the cache is free to flush b to main memory before a. There are a number of potential sources of reordering, such as the compiler, the JIT, and the cache. The compiler, runtime, and hardware are supposed to conspire to create the illusion of as-if-serial semantics, which means that in a single-threaded program, the program should not be able to observe the effects of reorderings. However, reorderings can come into play in incorrectly synchronized multithreaded programs, where one thread is able to observe the effects of other threads, and may be able to detect that variable accesses become visible to other threads in a different order than executed or specified in the program.

Most of the time, one thread doesn't care what the other is doing. But when it does, that's what synchronization is for.

Incorrectly synchronized code can be different things to different people. Incorrectly synchronized code in the context of the Java Memory Model means -
there is a write of a variable by one thread, there is a read of the same variable by another thread and
the write and read are not ordered by synchronization When these rules are violated, we say we have a race condition on that variable.
A program with a data race is an incorrectly synchronized program.

Synchronization has several aspects. The most well-understood is mutual exclusion -- only one thread can hold a monitor at once, so synchronizing on a monitor means, only one thread enters a synchronized block protected by a monitor, no other thread can enter a block protected by that monitor until the first thread exits the synchronized block.

But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.

The new memory model semantics create a partial ordering on memory operations (read field, write field, lock, unlock) and other thread operations (start and join), where some actions are said to happen before other operations. When one action happens before another, the first is guaranteed to be ordered before and visible to the second.

The rules of this ordering are as follows:
Each action in a thread "happens before" every action in that thread that comes later in the program's order.
An unlock on a monitor "happens before" every subsequent lock on that same monitor.
A write to a volatile field "happens before" every subsequent read of that same volatile.
A call to start() on a thread "happens before" any actions in the started thread.
All actions in a thread "happen before" any other thread successfully returns from a join() on that thread.

This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.

We have to set up a happens-before relationship for one thread to see the results of another.

Important Note: It is important for both threads to synchronize on the same monitor in order to set up the happens-before relationship properly. It is not the case that everything visible to thread A when it synchronizes on object X becomes visible to thread B after it synchronizes on object Y. The release and acquire have to "match" (i.e., be performed on the same monitor) to have the right semantics. Otherwise, the code has a data race.


Source: jsr-133-faq

Comments

Popular posts from this blog

StackOverFlowError and OutOfMemoryError in java

There are two area inside java virtual machine's memory the heap and the  stack . The  stack  memory is used to store local variables and function call while heap memory is used to store objects in  Java The most common cause of StackOverFlowError is too deep or infinite recursion or many local objects / variables creation inside function call in  Java.  According to the java source documentation,  Java throws  java.lang.StackOverflowError   when a stack overflow occurs because an application recurses too deeply. JVM has a given memory allocation for each stack of each thread, and if an attempt to call a method happens to fill this memory, JVM throws an error. Just like it would do if we try to write at index N of an array of length N.  The point to be noted here is that - These are errors not an exceptions. No memory corruption happens due to the error. Stack can not write into the heap space. A StackOverflowError i...

Job Sequencing with Deadlines

Given a set of n jobs Each job i has an integer deadlines di>=0 and a profit pi>0 All jobs requires only one unit time to complete Only one machine is available for processing jobs For job i the profit pi is earned if the job is completed by its deadline.