Java Memory Model Talk Notes

(This notes is from watching Java Memory Model by Jeremy Manson)

Java Memory Model

Introduction

Goal

Learn the building blocks of concurrency and how to design clever but correct concurrent abstractions and design patterns.

Scope

  • Fundamentals: Happens-before ordering
  • Using volatile
  • Thread safe lazy initialization
  • final fields
  • Recommendations

Java Thread Specification

  • Revised as part of JSR-133
  • Goals
    • Clear and easy to understand
    • Foster reliable multithreaded code
    • Allow for high performance JVMs

Safety Issues in Multithreaded Systems

  • Many intuitive assumptions do not hold
  • Some widely used idioms are not safe
    • Original Double-checked locking idiom
    • Checking non-volatile flag for thread termination
  • Can’t depend on testing to check for errors
    • Some anomalies will occur only on some platforms
    • Anomalies will occur rarely and non-repeatedly

This talk

  • Describe building blocks of synchronization and concurrent programming in Java language
  • Explain what it means for code to be correctly synchronized
  • Synchronized methods and blocks
  • Final fields and immutability

Taxonomy

High to low:

  • High level concurrency abstractions: JSR-166 and java.util.concurrent
  • Low level locking: synchronized() blocks and java.util.concurrent.locks
  • Low level primitives
    • volatile variables
    • java.util.concurrent.atomic classes
    • allows for non-blocking synchronization
  • Data races: deleberate undersynchronization
    • Avoid!
    • Not even Doug Lea can get it right

Happens-before ordering

Synchronization is needed for Blocking and Visibility

  • Synchronization isn’t just about mutual exclusion and blocking
  • It also regulates when other threads must see writes by other threads
    • When writes become visible
  • Without synchronization, compiler and processor are allowed to reorder memory access in ways that may surprise your

Don’t try to be too clever

  • Nearly impossible to do correctly to come with your solution

Quiz

Quiz

Sounds like impossible. But how?

  • The VM/compiler can reorder the statements.
  • Processor can reorder them
  • On multi-processor, values not synchronized to global memory
  • The momory model is designed to allow aggressive optimization
  • Good for performance
    • But bad for your intuition about insufficiently synchronized code!

When are actions visible to other threads?

When are actions visible

Release and Acquire

  • All memory accesses before a release
    • Are ordered before and visible to
    • Any memory accesses after a matching acquire
  • Unlocking a monitor/lock is a release
    • that is acquired by any following lock of that monitor/lock

Happens-before ordering

  • A release and a matching later acquire establish a happens-before ordering
  • Execution order within a thread also establishes a happens-before order
  • Happens-before order is transitive

Data race

If there are two accesses to a memory location

  • at least one of those accesses is a write, and
  • the momory location isn’t volatile, then

the access must be ordered by happens-before

Violate this, and it is ridiculously hard to figure out what your program can do

  • not as bad/unspecified as a buffer overflow in C, but it’s still super hard to figure what’s going on

Need something more accurate?

What does entering/leaving a synchronized block actually do?

Synchronization actions (approximately, or oversimplification)

1
2
3
4
5
6
7
8
9
10
11
int z = o.field1;
// block until obtain lock
synchronized(o) {
// get main memory value of field1 and field2
int x = o.field1;
int y = o.field2;
o.field3 = x + y;
// commit value of field3 to main memory
}
// release lock
moreCode();

Ordering

Roach motel ordering

  • Compiler/processor can move accesses into synchronized blocks
  • Can only move them out under special circumstances, generally not observable

But a release only matters to a matching acquire

Some special cases:

  • locks on thread local objects are a no-op
  • reentrant locks are a no-op
  • Java SE 6 does optimizations based on this

Using volatile

volatile fields

If a field could be simultaneously accessed by multiple threads, and at least one of those accesses is a write

Two choices:

  • use locking to prevent simultaneous access
  • make the field volatile
    • serves as documentation
    • gives essential JVM guarantees

Can be tricky to get volatile right, but nearly impossible without volatiole or synchronization

What does volatile do?

  • reads and writes go directly to memory
    • not cached in registers
  • volatile longs and doubles are atomic
    • not true for non-volatile longs and doubles
  • volatile reads/writes cannot be reordered
  • reads/writes become acquire/release pairs

Volatile happens-before edges

  • A volatile write happens-before all following reads of the same variable
  • A volatile write is similar to an unlock or monitor exit
    • in terms of the happens-before edge it creates
  • A volatile read is similar to a lock or monitor enter

volatile guarantees visibility

  • stop must be declared volatile
    • Otherwise, compiler could keep in register
1
2
3
4
5
6
7
8
9
10
11
class Animator implements Runnable {
private volatile boolean stop = false;
public void stop() { stop = true; }
public void run() {
while (!stop) {
oneStep();
try { Thread.sleep(100); } ...;
}
}
...
}

Compiler could ‘optimize’ the stop if volatile was not used.

volatile guarantees ordering

If a thread reads data, there is a happens-before edge from write to read of ready that guarantees visibility of data.

Other Happens-Before orderings

  • Starting a thread happens-before the run method of the thread
  • The termination of a thread happens-before a join with the terminated thread
  • Many util.concurrent methods set up happens-before orderings
    • placing an object into any concurrent collection happens-before the access or removal of that element from the collection

Thread safe lazy initialization

  • Want to perform lazy initialization of something that will be shared by man threads
  • Don’t want to pay for synchronization after object is initialized

Original Double Checked Locking (NOT WORK!)

1
2
3
4
5
6
7
8
9
10
11
12
Helper helper;
Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}

The problem is there are no lock acquire for reading!

Better Static Lazy Initialization

  • If you need to initialize a singleton value
    • Something that will only be initialized once, lazily, per JVM
  • Just initialize it in the declaration of a static variable
    • or in a static initialization block
    • See “Initialization on Demand Holder Idiom” in Effective Java
  • Spec guarantees it will be initialized in a thread safe way at the first use of that class
    • but not before
1
2
3
4
5
6
7
8
9
10
11
class Helper {
static final Helper helper = new Helper();
public static Helper getHelper() {
return helper;
}
private Helper() {
// Initialization...
}
}

Final fields

Thread-safe Immutable objects

  • Use immutable objects when you can
    • lots of advantages, including reducing needs for synchronization
  • Can make all fields final
    • don’t allow other threads to see object until construction complete
  • Gives added advantage
    • spec promises immutability, even if malicious code attacks you with data races

Data race attack

  • Thread 1 creates instances of a class
  • Thread 1 hands the instances to thread 2 without using synchronization
  • Thread 2 accesses the object
  • It is possible, although unlikely, that thread 2 could access an object before all the writes performed by the constructor in thread 1 are visible to thread 2

Recommendations

These are building blocks

  • If you can solve your problems using the high level concurrency abstractions provided by java.util.concurrent
    • do so
    • They’re safe! They’re fast! They slice! They dice!
  • Understanding the memory model, and what release/acquire means in that context, can help you devise and implement your own concurrency abstractons
    • and learn what not to do

Mostly, it just works

  • If you aren’t trying to be clever, the memory model just works and doesn’t surprise
    • no change from previous generally recommended programming practice
  • Known the details can
    • reassure those whose obsess over details
    • clarify the fine line between clever and stupid

Watch out for useless syncrhonization

  • Using a concurrent class in a single threaded context can generate measurable overhead
    • synchronization on each access to a Vector, or on each IO operation
  • Substitute unsynchronized classes when appropriate
    • ArrayList for Vector
  • Perform bulk I/O or use java.nio

Sometimes syncrhonization isn’t enough

Even if you use a concurrent class, your code may not be thread safe

1
2
3
4
5
6
7
8
9
10
ConcurrentHashMap<String, ID> h;
ID getID(String name) {
ID x = h.get(name);
if (x == null) {
x = new ID();
h.put(name, x);
}
return x;
}

Atomicity does not hold here

Be careful about where your concurrency logic goes

  • java.util.concurrent does not mean you should shoehorn something in there if it doesn’t fit
  • Make your synchronization orthogonal to app logic when possible
    • Use / make classes like SynchronizedMap
    • Can prevent atomicity problems with a “put-if-absent” wrapper in your synchronization code

Documenting concurrency

  • Often the concureency properties of a class are poorly documented
    • is an IO stream thread safe?
  • Not as simple as “this class is thread-safe”
  • Look at java.util.concurrent documentation
  • Look at annotations described in Java Concurrency in Practice
    • @ThreadSafe
    • @NotThreadSafe
    • @GuardedBy
    • @Immutable
    • Some of which are checked by FindBugs

Designing Fast Concurrent Code

  • Make it right before you make it fast
  • Reduce synchronization costs
    • avoid sharing mutable objects across threads
    • avoid old Collection classes (Vector, Hashtable)
    • use bulk I/O (or java.nio)
  • Use java.util.concurrent classes
    • designed for speed, scalability, and correctness
  • Avoid lock contention
    • Reduce lock scopes
    • Reduce lock durations

Wrap-up

  • Cost of synchronization operations can be significant
    • But cost of needed synchronization rarely is
  • Thread interaction needs careful thought
    • But not too clever
    • Don’t want to have to think too hard about reordering
      • If you don’t have data races, you don’t have to think about the weird things the compiler is allowed to do
  • Communication between threads
    • Requires a happens-before edge/ordering
    • Both threads must participate
    • No way for one thread to push information into other threads
      • final fields allow some guaranteed communications without a normal happens-before edge, but don’t write code that depends on this for normal operations

For more information