Table of Contents
Key Ideas
- All threads share the main memory
- Each thread uses a local working memory
- Flushing and Refreshing working memory to and from the main memory must comply with the Memory Model
Java Memory Model
- JMM specifies guarantees given by the JVM
- About when writes to variables become visible to other threads
- JMM is an abstraction on top of the hardware memory model -> we can program once and don't care about if the architecture is x86 or ARM.
Memory Model Guarantees
- Atomicity
- Which operations are indivisible?
- Visibility
- Under which condition are the effects of operations executed by one thread viswible to other threads ?
- Releasing a lock forces the thread to flush out all pending writes
- Obtaining a lock forces the thread to get fresh values from memory
- The start of a thread implies a flush of pending writes in the starting thread
- Started thread will get fresh values from memory before run is executed
- The end of a thread implies a flush
- A thread which is informed about the end of another thread (join, isAlive) sees the changes performed by the terminated thread on shared fields
- Under which condition are the effects of operations executed by one thread viswible to other threads ?
- Ordering
- Under which conditions can the effects of operations appear out of oder to any given thread? Related to visibility.
JMM does not guarantee sequential consistency. Sequential is easy to understand but unpractical, caching mechanisms would be useless and reorderings by the compiler impossible.
JLS only requires the JVM to maintain within-thread as-if-serial semantics. This means that as long as a thread has the same result as if it were executed in a program order in a strictly sequential environment, caching and reordering is permissible => we need ruls how inter thread memory actions are processed!
Stopping threads worksheet explained
In the worksheet there was thread with a variable running = false. Yet the thread was still not terminated. Why?
- StoppableThread caches a copy of the running flag in its cpu register
- main Thread writes
running = falseto the heap - StoppableThread does not see the modifications made by the main thread
- For variables which are marked
volatile, local caching is not allowed! This solves the problem described.- Guarantees visibility of writes
- reads / writes of volatile longs / doubles are guaranteed to be atomic
- May or may have not costs depending on the architecture
Double Checked Locking Problem
Lazy loading the Singleton
Example scenario: We have a Singleton Pattern and try to lazy load it:
public class Singleton() {
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() { /* init */ }
//other methods
}
What's the problem now? If two Threads try to access the getInstance() method, both may read that instance is still null and therefore create it. Now we broke the Singleton pattern.
Solution: make it synchronized. But this solution is expensive. Every getInstance call will be synchronized but we actually only need it for the first call.
Better solution?: Singleton with double checked locking:
public class Singleton() {
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.Class){
if(instance == null) {
instance = new Singleton(); //here
}
}
}
return instance;
}
private Singleton() { /* init */ }
//other methods
}
We first do a cheap check to see if its null or not. If it's actually null we do one expensive check to see if its actually null or not. Sadly this does not work perfectly. We have a visibility problem (at here) due to the first instance == null check being not synchronized. Therefore the work behind the scenes (malloc, init, address) may not be visible in the right order.
Actual solution: declare instance it volatile.
Why is there not a problem with eager loading?
Let's take the following eager initialization of the singleton:
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() { return instance; }
private Singleton() { /*init */ }
//other methods
}
Eager loading of the singleton is not a problem because the instance is created at the time of class loading, ensuring thread safety without requiring synchronization. The JVM guarantees that the static initialization of a class is thread-safe, meaning the instance is created before any thread accesses it. This eliminates the race condition present in the lazy loading approach, where multiple threads might simultaneously check for null and attempt to create the instance. Additionally, eager loading avoids the visibility issues caused by reordering or caching, as the instance is fully initialized before it is accessed.