The Java Memory Model Computer Science Essay

Published:

This essay has been submitted by a student. This is not an example of the work written by our professional essay writers.

Speed , efficiency and optimal use of resources are some of the factors that play a key role in mordern computer programming. It involves algotithm design and code structuring in a way so as to ensure that the program runs in the most efficient way to produce the desired results. But some times it may so happen that these goals contradict each other.

In a single threaded program a care needs to be taken by the compiler while making program transformations so as to not hamper the possible results of the program. This is referred to as preserving the intra-thread semantics of the program by the compiler. A lone thread has to behave as if no code transformation occurred at all. For a single thread program it is fairly easy by comparison. It simply needs to make sure that when an instruction is performed, it does not effect the results any of the instructions past which it was moved. Generally for single threaded program , the programmers need not reason about the potential reordering. The real difficulty comes when there are multiple threads of instructions executing at the same time, as well as interacting. In such cases, the limiting factors we applied for single-threaded programming is not sufficient - it can result in abnormal and at times weird results. Compilers that do not take into account the existence of multiple threads can perform transformations that would violate the semantics of even correctly synchronized programs. In modern multithreaded programming practices , the programmer states ordering constraints by clearly describing how threads communicate with each other.

These issues are addressed in the memory model which describes the communication between the program and memory. For multithreaded systems , the memory model specifies how the memory actions (e.g., reads and writes) in a program will appear to execute and what values will be returned in each read of the memory location.

Memory Model is a must for every hardware and software interface of a system that provides multithreaded access to shared memory. The transformations done to a piece of code by the system (compiler, virtual machine, or hardware) is also largely determined by the memory model. The memory model in Java establishes the alterations that might be applied to the code when producing bytecode , and subsequently while producing native code from bytecode and finally the optimizations that the hardware may perform on the native code.

The model has a significant impact on the programmers ; the transformation determines the possible outcomes of a program, which in turn determines design patterns that are acceptable for communicating between the threads. Without a well established memory model for a programming language, it is impossible to know what the results are for a program in that language.

The designers of JAVA programming language aimed to have the multithreaded programs written in Java to have a steady, consistent and well-defined behavior. This would enable the programmers to have a better understanding of their programs actions and would provide help to the Java architects to build the platforms in a flexible and competent manner , all the while ensuring that the Java programs ran on them correctly. Unfortunately the original memory model was not defined in a way that would allow the programmers and architects to have a clear understanding of the requirements of the Java System. It did not allow many standard compiler transformation.

Problems with the JMM introduced in 1995

The first specification for the Java memory model came out in the year 1995. The original Java Memory Model was not well specified and difficult to understand. There were several flaws in the first Java Memory Model some of which are explained below:

Firstly, the Java Memory Model introduced in 1995 was poorly specified. It was very difficult for the programmers and compiler designers to comprehend which optimizations and execution-patterns were permitted under the model. Different people developed different understanding of the directives of the JMM which led to several variations in the different implementations of the underlying compilers.

The way the JMM treated final objects or variables was no different than the normal ones. The perception of the programmers was that if a variable has been declared final then it will automatically be synchronized with respect to different threads. This was kind of a "normal expectation" from the final fields. However, the JMM did not have any provision for this behavior. The only way to make sure that accessing the final fields will give a value that has been set by the constructor and not the default value of the variables is to use synchronization.

The semantics of the volatile fields was also not properly specified in the old JMM. It allowed volatile writes to be reordered with nonvolatile reads and writes which was counter-intuitive for most developers. For example, considering the following code snippet to be a part of a multithreaded program:

class Example {

int count = 0;

volatile boolean flag = false;

public void writeData() {

count = 42;

flag = true;

}

public void readData() {

if (flag == true) {

System.out.println("The value of count is : " + count);

}

}

}

In the above example class, if two different threads are calling the functions readData() and writeData(), the outputs given by the old and the new JMM differ. Once the write operation is performed on the flag variable in the writeData() function, the write to the variable count is reflected in the memory. The read operation on flag returns this value of count from the memory. In the current JMM, if the value of flag is returned to be true, it can be safely assumed that the value returned for count would be 42. However, in case of the old JMM, the reads and writes to volatile variables were allowed to be reordered with respect to the read and write operations on normal variables. Therefore, when run on the old JMM, it was possible that the write operation to flag would take place before the value of 42 is written to count. In that case, the call to the function readData() would return 0.

Another problem with the old JMM was that it constrained some of the very common reorderings that were incorporated into most of the existing JVMs. Also, as it was difficult to understand, in many of the cases the rules were violated. One more concern was that in some of the cases, the programs would seem to run fine even when they were incorrectly synchronized.

All these limitations of the old JMM gave way to the need to specify a new memory model for Java.

Redesign of JMM in 2004

In order to remove the flaws in JMM, a new specification - JSR 133 - was proposed in 2004. The newly specified memory model was packaged with the Java 5.0 (Tiger) release. JSR 133, chartered to fix the JMM, had the following goals:

Preserve existing safety guarantees, including type-safety.

Provide out-of-thin-air safety : This means that variable values are not created "out of thin air" - so for a thread to observe a variable to have value X, some thread must have actually written the value X to that variable in the past.

The semantics of "correctly synchronized" programs should be as simple and intuitive as feasible. Thus, "correctly synchronized" should be defined both formally and intuitively and the two definitions should be consistent with each other.

Programmers should be able to create multithreaded programs with confidence that they will be reliable and correct. Of course, there is no magic that makes writing concurrent applications easy, but the goal is to relieve application writers of the burden of understanding all the subtleties of the memory model.

High performance JVM implementations across a wide range of popular hardware architectures should be possible. Modern processors differ substantially in their memory models; the JMM should accommodate as many possible architectures as practical, without sacrificing performance.

There should be minimal impact on the existing code. 

Features of the JMM as of today

The new memory model for a multi threaded system specifies how memory actions like reads and writes in a program will appear to execute to the programmer. The new memory model also impacts the programmer. The new model shows what transformations it allows and also helps to determine the possible output of the program. This helps in finding which design patterns between threads are legal. The new memory model follows data-race-free approach for correct programs. Data-race-free programs are said to be correct programs. The safety and security properties of java are the major contributions of the revised java memory model. The revised memory model follows a new technique which allows standard optimization prohibiting the necessary executions. Legal executions are built iteratively. The new model commits itself to a set of memory actions in each of the iteration. The memory actions are said to be committed if they occur in a well-behaved execution manner.

REQUIREMENTS FOR THE NEW JAVA MEMORY MODEL:

The major aim of the new memory model is to provide proper balance between sufficient ease of use and between the transformations and optimizations used in current compilers in hardware.

CORRECTLY SYNCHRONIZED PROGRAMS:

Conflicting accesses: an access to a variable is defined by the read and writes performed to the variable. Conflicting access is said to happen when two accesses to the same shared file conflicts with one of the access being write.

Synchronization actions: locks, unlocks, reads and writes of volatile variables are said to be synchronization actions.

Synchronization order: the total order over all synchronization action is called synchronization order.

Happens Before Order: for two actions x and y, x→y is used to indicate that x happens before y.

Data Race: Data race is said to happen when in an execution, two accesses from different threads conflict and they are not ordered by happens before.

OUT-OF-THIN AIR GUARANTEES FOR INCORRECT PROGRAMS:

The program semantics must be clearly defined. If a programmer doesn't know what their code is doing, he will not be able to know what their code is doing wrong. The second major requirement of java memory model is to clearly define the semantics for how the code should behave when a program is not correctly written and without affecting the compilers and hardware. To clearly characterize the out-of -thin air violation is complicated.

HAPPENS-BEFORE MEMORY MODEL:

If two actions x and y are said to happen, they are ordered by happens-before relationship. hb(x,y) is used to indicate that x happens before y and the first is visible to and ordered before the second. For example: The write of a default value to every object constructed by a thread need not happen before the beginning of the thread. If x and y share a happens-before relationship, they do not necessarily have to have happened in the order of the code. Writes in one thread which is in data race with the read in other thread may appear to occur out of order.

DATA AND CONTROL DEPENDENCIES:

The missing link between happens-before memory model and our desired semantics is one of causality. Identifying what constitutes to a well-behaved execution is the main key to our notion of causality. To maintain the consistency among successive executions it requires that in all successive executions, the happens-before and synchronization order among the committed access remains the same and the values returned by the committed reads also remains the same.

FORMAL SPECIFICATION OF ACTIONS AND EXECUTIONS:

an action a is described by the tuple (t,k,v,u) where,

t- Thread performing the action,

k- The kind of action involved like volatile read, volatile write, read, write, lock, unlock special synchronization actions, external actions.

v- The variable involved in action.

u- An arbitrary unique identifier of the action.

OBSERVABLE BEHAVIOUR AND NONTERMINATING EXECUTIONS:

The finite set of external actions in a program defines the observable behavior. For example: if a programs prints "HELLO" forever is said to have a set of behavior for non-negative integer i, and includes the printing of "HELLO" i times. The termination of a program is not modeled as a behavior, but a program can be easily extended to generate additional action "execution termination" when all threads have terminated.

Programs can hang when:

All non-terminated threads have been blocked,

at least one blocked thread exists,

The program can perform an unbounded number of actions without performing any external actions.

A thread can be blocked in many circumstances, such as when it is attempting to acquire a lock or perform an external action that depends on an external data. If a thread is in a blocked state, Thread.getState will return the thread to BLOCKED or WAITING state.

VOLATILE FIELDS:

Synchronization is a term which is very closely related to and dependent on the Java Memory Model. Volatile Fields is one of the features of java which have been strengthened in the new memory model. One field can be accessed simultaneously by multiple threads and one of those accesses might be a write operation. In this case a programmer would have two options, either to do something to prevent simultaneous accesses using locks or to make use of volatile fields to make sure the value fetched for the field would always be consistent. The new JMM specifies that the read and write operations to volatile variables can never be reordered with other normal reads and writes. These reads and writes to the volatile variables go directly to memory and are not cached into registers. These reads and writes act as acquire and release i.e. volatile read is similar to an unlock or monitor exit whereas volatile write is similar to a lock or monitor enter.

FINAL FIELDS: [http://java.sun.com/docs]

The final keyword is another important feature whose implementation was re-specified in the new Java Memory Model. The final fields in Java are initialized once and are never changed. The fields which are declared as final, don't allow other threads to fetch the object's value until that particular object's construction is complete. Java Memory Model also promises that object is going to be correctly constructed and also it will be immutable to all the other threads.

Example:

class FinalFields

{

final int a;

int b;

static FinalFields obj;

public FinalFields()

{

a = 3;

b = 4;

}

static void write()

{

obj = new FinalFields();

}

static void read()

{

if (obj != null)

{

int c = obj.a;

int d = obj.b;

}

}

}

In the above example, there are two methods i.e. write() and read(), one thread might execute read() and another thread might execute write(). The class FinalFields has one final integer field a and one non-final field b. Object constructer finishes when the write() method writes final and the read() method will definitely give the initialized output for obj.a. i.e the value 3 because it is declared as final. However as obj.b is not final, its not for sure that the read() method would see the value 4 for it; it could see the value 0 as well if it tries to access it before the constructor has initialized the variable.

An example to explain how multithreading is implemented in Java

class BankAccount

{

      volatile int balance;

     

      BankAccount()

      {

            balance = 10000;

      }

     

      synchronized public void withdraw()

      {

            System.out.println(Thread.currentThread().getName() + " is trying to withdraw 7000 £ from the account");

           

            if(balance < 7000)

            {

                  System.out.println("There is not enough balance in the account for " + Thread.currentThread().getName() + " to withdraw 7000 £");

            }

            else

            {

                  balance = balance - 7000;

                  System.out.println("Balance after completion of " + Thread.currentThread().getName() + "'s operation is : " + balance);

            }

      }

     

      public void deposit()

      {

            balance = balance + 2000;

      }

}

 

class CustomerInstance implements Runnable

{

      BankAccount obj;

     

      CustomerInstance(BankAccount o)

      {

            obj = o;

            new Thread(this,"Alice").start();

            System.out.println("Alice logged into the account");

            new Thread(this,"Bob").start();

            System.out.println("Bob logged into the account");

      }

 

      public void run()

      {

            obj.withdraw();

      }

}

 

public class Customer

{    

      public static void main(String[] args)

      {

            BankAccount a;

            a = new BankAccount();

            new CustomerInstance(a);

      }

}

 

 

The above program shows how to implement a simple multi-threaded application in Java with the new memory model specification. It is a simplified implementation for a bank account with a field balance which stores the current balance in the account in pounds. The withdraw() method decrements the balance by 7000 pounds. There might be a scenario in which two people (Alice and Bob in the program) are trying to access the same account through its internet banking portal. Both of them are trying to withdraw 7000 pounds from the account and transfer to some other account. Thus there would be two different threads trying to decrement the balance by 7000 pounds at the same time. This is a classic and very simplified example of a data-race condition. The thread which gets access to the withdraw() method first would get to withdraw the money. As the method is synchronised, no two threads can get access to it at the same time. The person who gets the control second, will get an error message which says "there is not enough balance in the account".

 

Two of the possible outcomes of the program are as below:

Alice logged into the account

Bob logged into the account

Bob is trying to withdraw 7000 £ from the account

Balance after completion of Bob's operation is: 3000

Alice is trying to withdraw 7000 £ from the account

There is not enough balance in the account for Alice to withdraw 7000 £

 

Alice logged into the account

Bob logged into the account

Alice is trying to withdraw 7000 £ from the account

Balance after completion of Alice's operation is: 3000

Bob is trying to withdraw 7000 £ from the account

There is not enough balance in the account for Bob to withdraw 7000 £

Software Tools

CHESS: Created by Microsoft Research. It detects concurrency errors by systematically exploring thread schedules and interleaving. It is capable of finding race conditions, deadlocks and data corruption issues. CHESS runs a regular unit test repeatedly on a specialized scheduler. On every repetition, it chooses a different scheduling order. As a model checker, it controls the specialized scheduler that is capable of creating specific thread interleaving. To control the state space explosion, CHESS applies partial-order reduction and a novel iteration context bounding.

In iteration context bounding, instead of limiting the state space explosion by depth, CHESS limits number of thread switches in a given execution. The thread itself can run any number of steps between thread switches, leaving the execution depth unbounded (a big win over traditional model checking). This is based on the empirical evidence that a small number of systematic thread switches is sufficient to expose most concurrency bugs.

CHESS is a boon for all those developers and testers who had to solely rely on stress for better interleaved testing. It takes regular unit tests and methodically simulates interesting interleaving.

THE INTEL THREAD CHECKER: This is a dynamic analysis tool for finding deadlocks, data races, and incorrect uses of the native Windows synchronization APIs. The Thread Checker needs to instrument either the source code or the compiled binary to make every memory reference and every standard Win32 synchronization primitive observable. At run time, the instrumented binary provides sufficient information for the analyzer to construct a partial-order of execution. The tool then performs a "happens-before" analysis on the partial order. Please refer to the "Race Detection Algorithms" sidebar for more information on happens-before analysis.

For performance and scalability reasons, instead of remembering all accesses to a shared variable, the tool only remembers recent accesses. This helps to improve the tool's efficiency while analyzing long-running applications. However, the side effect is that it will miss some bugs. It's a trade-off, and perhaps it's more important to find many bugs in long-running applications than to find all bugs in a very short-lived application.

RACERX: This flow-sensitive static analysis tool is used for detecting races and deadlocks. It overcomes the requirement to tediously annotate the entire source code. In fact, the only annotation requirement is that users provide a table specifying APIs used to acquire and release locks. The locking primitives' attributes such as spinning, blocking, and re-entrancy can also be specified. This table will typically be very small, 30 entries at most. This tremendously reduces the burden of source annotation for large systems.

In the first phase, RacerX iterates over each source code file and builds a Control Flow Graph (CFG). The CFG has information related to function calls, shared memory, use of pointers, and other data. As it builds the CFG, it also refers back to the table of synchronization primitives and marks calls to these APIs.

Once the complete CFG is built, the analysis phase kicks in, which includes running the race checker and deadlock checker. As the context flow is traversed, the lockset algorithm is used to catch potential race conditions. For deadlock analysis, it computes locking cycles every time locks are taken.

The final phase involves post-processing all the reported errors in order to prioritize by importance and harmfulness of the error.

CHORD: This is a flow-insensitive, context-sensitive static analysis tool for Java. Being flow-insensitive allows it be far more scalable than other static tools, but at the cost of losing precision. It also takes into account the specific synchronization primitives available in Java.

ZING: This tool is a pure model checker meant for design verification of multi threaded programs. Zing has its own custom language that is used to describe complex states and transition, and it is fully capable of modeling concurrent state machines. Like other model checkers, Zing provides a comprehensive way to verify designs; it also helps build confidence in the quality of the design since you can verify assumptions and formally prove the presence or absence of certain conditions. It also contends with concurrent state space explosion by innovative reduction techniques.

Summary

Java Memory Model which was released in 1995 was flawed due to undesirable changes in reasonable-looking programs. It was too difficult to write concurrent classes properly, and then we were guaranteed that many concurrent classes would not work as expected. Fortunately, JSR 133 process created a memory model with new specifications for writing synchronized programs according to developer's perspective. These changes to the memory model, which were incorporated into Java 5.0, redefined the semantics of threads, synchronization, volatile variables, and final fields. The new memory model provides efficient and provably correct techniques for safely and correctly implementing concurrent operations. However, there are some axioms like double-check locking which are still not fixed in the latest release of Java Memory Model.

Writing Services

Essay Writing
Service

Find out how the very best essay writing service can help you accomplish more and achieve higher marks today.

Assignment Writing Service

From complicated assignments to tricky tasks, our experts can tackle virtually any question thrown at them.

Dissertation Writing Service

A dissertation (also known as a thesis or research project) is probably the most important piece of work for any student! From full dissertations to individual chapters, we’re on hand to support you.

Coursework Writing Service

Our expert qualified writers can help you get your coursework right first time, every time.

Dissertation Proposal Service

The first step to completing a dissertation is to create a proposal that talks about what you wish to do. Our experts can design suitable methodologies - perfect to help you get started with a dissertation.

Report Writing
Service

Reports for any audience. Perfectly structured, professionally written, and tailored to suit your exact requirements.

Essay Skeleton Answer Service

If you’re just looking for some help to get started on an essay, our outline service provides you with a perfect essay plan.

Marking & Proofreading Service

Not sure if your work is hitting the mark? Struggling to get feedback from your lecturer? Our premium marking service was created just for you - get the feedback you deserve now.

Exam Revision
Service

Exams can be one of the most stressful experiences you’ll ever have! Revision is key, and we’re here to help. With custom created revision notes and exam answers, you’ll never feel underprepared again.