MrAndroid
9 min readDec 3, 2022

Java | Multithreading Part 1: Java Memory Model

This material will be useful to those who have begun to study how the Java memory model works or to those who are preparing for a technical interview.

The material is presented in the form of a series of articles.

The memory model exists both at the language level and at the processor level, but they are not directly related. The language model can provide both weaker and stronger guarantees than the processor model.

What is Java Memory Model?

The Java Memory Model (JMM) describes the behavior of a program in a multithreaded environment.

The JVM defines how the Java virtual machine works with computer memory (RAM) and explains the possible behavior of threads and what a programmer should rely on when developing an application.

Computer Hardware Memory:

Current hardware memory architecture is different than java memory model. It’s important to understand how JVM work with hardware memory.

Modern computers have several powerful processors. Each of the processors interacts with computer memory. The processor loads data from memory, processes it, and then writes that data to memory.

But RAM has a slower memory access speed than the processor can handle.

There is a situation in which the speed of the processor and the speed of memory access has a significant difference.

To solve the problem of the fact that the processor is in standby mode while it is accessing data in memory, the processors have added their own cache.

The cache consists of allocated memory for each CPU and register.
A cache is faster than accessing the main memory but slower than accessing its internal registers.

Some CPUs have multiple levels of cache (Level 1, Level 2, etc.).

How does the CPU read and write data to main memory?
1) CPU reads data from main memory into it’s cache;
2) CPU may read data into it’s internal register from it’s cache;
3) CPU performs operations on the data;
4) CPU writes data back to the cache if necessary;
5) CPU writes data from the cache to main memory as needed.

Interaction between CPU and memory

When does the CPU cache clear?
This happens when the cache is full and the data stored in it is about to be overwritten by new data being loaded into the cache.

The Internal Java Memory Model:

The JVM divides memory between thread stack and the heap.

What is a thread stack?

Each thread has its own stack, a memory are a that stores local variables, object references, and information about which methods the thread has called.

Local primitive variables on the stack are only visible to the thread that owns it. Even if two threads are executing the same code, they will still create local variables for that code on their own stacks. Thus, each thread has its own version of each local variable.

Local reference variables, referencing objects on the heap, also only visible to the thread that owns it.

What is the heap?

JVM heap is an independent memory allocation.

The heap contains all objects created by the application. Objects reside here regardless of which thread created them.

Objects on the heap are visible to all threads.

Internal Java Memory Model

What are the differences between stack and heap in terms of multithreading?

Stack is part of memory witch visibility only for thread. Every thread has your own stack, stack keep locale variables, variables of methods and call stack. Variables in stack not visible for other threads.

Heap is common part of memory. All objects is creating in heap.

To improve performance, thread can caches values from the heap onto its stack.

The Java Memory Model and how it maps to Computer Hardware Memory

Variables referenced by the stack of thread and heap can be cached in all levels of the computer’s memory.

JVMl and Computer Hardware Memory

This behaiver of keep data in memroy can create problems:

First, threads can lose visibility updates, what was made be other threads to shared objects.

Second, it’s race conditions. Threads can hold on to stale shared variable state and threads will be see unsynchronized data.

Thread Caching:

Thread can only access its own stack. Local variables created by a thread are not visible for other threads than the thread owner that created it.

All local primitive types(boolean, byte, short, char, int, long, float, double) save in thread stack.

The heap contains all the objects created in your Java application.

Local variable can be of a primitive type and in this case it is stored on the thread stack or local variable can also be a reference to an object. In this case, the reference (local variable) is stored on the thread stack, but the object itself is stored on the heap.

Object can have methods and this methods can have local variables. This local variables will stored in stack of thread, even if the object is stored on the heap.

Local varibles of object will stored in heap. It doesn’t matter it’s primitive variable or referensh on objects.

Static class variables are also stored on the heap.

If two threads share an object at the same time without synchronization or a proper object update message, then updates to the shared object made by one thread may not be visible to other threads.

For example:

Let’s imagine that ThreadA running on CPU1 reads data and caches it in its register while making some changes to this data.

At the same time, ThreadB that is running on CPU2 is also reading this object’s data.

A situation arises in which each of the threads has its own copy of the shared object, each copy is in the CPU cache.

This situation will continue until each of the CPUs has flushed its cache to the main memory (RAM).

A program must be correctly synchronized to avoid the kinds of counterintuitive behaviors. Java includes certain language constructs that help solve concurrency problems.

Visibility, Atomicity and Ordering :

These are fundamental to correct concurrent programming. As a rule, concurrent errors associated with these categories.

Visibility:

Visibility — determines when activities in one thread are made visible from another thread.

Java Memory Model makes promises objects that are created or altered in one thread may be visible in another thread. And doesn’t matter how many CPU’s or cores we have.

For example:

class Worker {
boolean done = false;

void work() {
while (!done) {
// do work
}
}

void stopWork() {
done = true;
}
}

Now imagine that two threads(ThreadA, ThreadB) are created, and ThreadA call’s method work(), and at some point, ThreadB calls stopWork().

Because there is no happens-before relationship between the two threads, the thread in the loop may never see the update to done performed by the other thread.

happens-before means that all changes that were made in ThreadB that this operation entailed are visible to ThreadA at the time of its execution.

In practice, this may happen if the compiler detects that no writes are performed to done in the first thread;
The compiler may hoist the read of done out of the loop, transforming it into an infinite loop.

Atomicity:

Atomicity —atomicity of operations. An atomic operation looks like a single and indivisible processor instruction, which may be either already executed or not yet executed.

Atomicity can be applied to a sequence of actions.

Consider the following example.

public class UserBankAccount {
private int balance;

synchronized int getBalance() {
return balance;
}

synchronized void setBalance(int x) throws IllegalStateException {
balance = x;
if (balance < 0) {
throw new IllegalStateException("Negative Balance");
}
}

void deposit(int x) {
int b = getBalance();
setBalance(b + x);
}

void withdraw(int x) {
int b = getBalance();
setBalance(b - x);
}
}

Now assume that threadA calls deposit(5), while another ThreadB calls withdraw(5). There is an initial balance of 10. Ideally, at the end of these two calls, there would still be a balance of 10.

The following situation may occur:
ThreadA gets a balance value of 10. At the same time, ThreadB calls the “withdraw” method and writes a balance of 5.
But ThreadA saved the balance in variable b as 10 and after adding 5 will write 15.
As a result of this lack of atomicity, the balance is 15 instead of 10.
For this example, making the deposit() and withdraw() methods synchronized will ensure that the actions of those methods take place atomically.

The second case:

Java allows 64-bit long and double values to be treated as two 32-bit values. A long write is a 64-bit write operation that is executed as two separate 32-bit operations.

public class Balance {
private long balance;

void set(long value) {
balance = value;
}

long get() {
return balance;
}
}

Imagine that we have two streams ThreadA and ThreadB. ThreadA do work for set new value for balance and ThreadB do work for get balance and procces it value.
If you do not synchronize the code written above. This can lead to a situation where ThreadB sees the first 32 bits of a 64-bit value from one entry and the second 32 bits from another entry.

Ordering:

We don’t have guarantee which jobs are executed first or last.

Compilers can reorder processor instructions to improve performance and execute them in random order until no difference is visible to the thread inside.

Consider what happens if threadOne() gets executed in one thread and threadTwo() gets executed in another.

Would it be possible for threadTwo() to return the value true?

public class BadlyOrdered {
boolean a = false;
boolean b = false;

void threadOne() {
a = true;
b = true;
}

boolean threadTwo() {
boolean r1 = b; // sees true
boolean r2 = a; // sees false
return r1 && !r2; // returns true
}
}

If ordering is not guaranteed, then the assignments to a and b in threadOne() can be performed out of order.
Compilers have substantial freedom to reorder code in the absence of synchronization. This might result in threadTwo() being executed after the assignment to b, but before the assignment to a.

This happens because in the threadOne method, the write operation data in the variables a and b are in no way dependent on each other and for one thread, it does not matter in what order the values ​​of these variables will be assigned.

How do you fix it?

To do this, we should understand how work threads and synchronize our code.

What is Thread-Safety ?

Thread-Safety is the process to make our program safe to use in multithreaded environment.

For example, we have some user balance to which we access from different threads for reading and writing data. Regardless of how many threads work with it, we guarantee that all threads will receive its up-to-date data.

There are different ways through which we can make our program thread safe. It’s will be in next topics.

What is difference between concurrency and parallelism?

Concurrency is when some tasks can start, run, and complete in some unspecified order. It can be be interleaving instructions via time slicing.

Concurrency

Parallelism happening at literally the same time. This requires hardware support (coprocessors, multi-core processors).

Parallelism

All parallelism is concurrent, but not all concurrency is parallel.

Conclusions:

We have studied how works Java Memory Model and how it maps to Computer Hardware Memory and with problems we can have if your code not synchronized