PV168
Memory Model
Memory Model
In computing, a memory model describes the interactions of threads through memory and their shared use of the data.[1]
- Essential for any programming environment using parallelism.
- Consists of:
- Language specification
- Compiler specification (including optimizations)
- Hardware (JVM in Java)
Memory Model
- Specifies: [2]
- synchronization points and actions
- what is affected by synchronization points
- what applies to memory access
- before the synchronization point
- after the synchronization point
- Defines Sequential Consistency and Happens-before relation.
Sequential Consistency
Compilers are allowed to reorder the instructions, when this does not affect the execution of the thread in isolation. [2]
- Also, the JIT may reorder, delay, or cache accesses.
- Also, the JVM may reorder, delay, or cache accesses.
- Also, the CPU may reorder, delay, or cache accesses.
Sequential Consistency
A = B = 0
// thread 1
r2 = A;
B = 1
// thread 2
r1 = B;
A = 2;
- What are the values of
r1 and r2?
- It may happen that
r1 == 1 and r2 == 2.
Synchronization Points
- Java has 4 types:
volatile
- Atomic classes
synchronized
- starts and joins of threads
Actions
- Action types:
- Load
- Store
- Synchronization
- Loads without any store are trouble-free.
- Any store requires a proper synchronization.
- Including all load accesses!
- Synchronization locks and unlocks the monitor.
Shared variables
- Shared variables (e.g. affected by the Memory Model):
- instance fields
static fields
- array elements
- Variables unaffected by the Memory Model:
- local variables
- methods arguments, caught exceptions
Happens-before
- Transitive relationship of actions.
-
If one action happens-before another, then the first is visible to and ordered before the second [3]
- Applies to all synchronization points.
- Partially applies to all
final fields.
- After construction, no further synchronization is needed.
volatile
- Implements the memory barrier:
- accesses to
volatiles cannot be reordered
- subsequent accesses cannot be reordered before
volatile load
- prior accesses cannot be reordered after
volatile store
- Numeric operations are not atomic (
++, --)
- DO NOT USE IT UNLESS YOU KNOW WHAT YOU ARE DOING!
volatile
class VolatileExample {
int x = 0;
volatile boolean flag = false;
public void writer() {
x = 42;
flag = true;
}
public void reader() {
if (flag == true) {
System.out.println(x); // guaranteed to see 42.
}
}
}
Atomic classes
- Useful for numeric counters, gauges, and indicators.
- Thread safe for all basic numeric operations.
- Increments, decrements, assignment
- “Synchronizes properly as you would expect.”
- Almost the same rules applies as for
volatiles.
- Allows finer specification of memory ordering.
- DO NOT USE FOR OTHER THINGS THAN COUNTERS UNLESS YOU KNOW WHAT YOU ARE DOING!
Atomic classes
class AtomicExample {
private int value = 0;
private AtomicBoolean flag = new AtomicBoolean(false);
public void writer() {
value = 42;
flag.set(true);
}
public void reader() {
while (!flag.get()) {} // busy wait here
System.out.println(value); // guaranteed to see 42.
}
}
synchronized
- When used in a method signature, it uses the monitor of the instance.
- Or monitor of the class in case the method is
static.
- When used in the body of a method, it uses the monitor of the specified object.
public synchronized void doSomething() { /* ... */ }
public void doSomethingElse() {
synchronized(this.attribute) {/* ... */}
}
Threads
- Thread start and join have similar memory semantics as
synchronized:
- Any store which happened-before the start of the thread is visible by the thread.
- Any store which happened-before the join of the about-to-join thread is visible in the joinee thread.
Threads
class ThreadExample {
public void run() {
Integer value = 42;
var t = new Thread(() -> value *= 2 );
// value is 42
t.start();
t.join();
// value is guaranteed to be 84;
}
}
final Fields
- Special rules apply to
final fields: [4]
- Set the final fields for an object in that object’s constructor.
- Do not write a reference to the object being constructed in a place where another thread can see it before the object’s constructor is finished.
- If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object’s final fields.
final Fields
- Do not reflectively modify
final fields! [5]
Double-Checked Locking
Is this correct?
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
Double-Checked Locking Fixed
Notice the volatile keyword.
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
Double-Checked Locking Fixed 2
public class Foo {
private Foo() {}
private static class LazyHolder {
static final Helper INSTANCE = new Helper();
}
public static Helper getHelper() {
return LazyHolder.INSTANCE;
}
}
How To Test The Correct Usage?
- Unit tests are insufficient.
- In fact, no commonly used automatization approaches can validate the correctness.
- To prove the correctness, one must provide a formal proof.
- This is the reason airplanes work in strictly single-threaded environments.
Without correct synchronization, very strange, confusing and counterintuitive behaviors are possible.
PV168
Course Summary
What have you experienced?
- Let’s try to remember what has happened in PV168
- What do you think about the real purpose of all that?
Programming is hard
- It’s not that difficult to fulfill
- compiler’s rules
- current requirements
- However, it’s a completely different game
- when other people are involved
- and requirements evolve over time
Essential vs. Accidental Complexity
- Essential complexity is integral part of the problem
- cannot be eliminated completely
- can only be minimized
- Accidental complexity is what we add to the problem
- by using inefficient tools
- by using inefficient methodologies
- by obscuring the intent in the code
Naming things is hard
- Good names reveal the intent (semantics)
- not the data type used (e.g.
listEvent)
- not the physical position (e.g.
northPanel)
- Good names lie in the problem domain
- not in the solution domain (Computer Science “jargon”)
- Naming things remain hard
- Naming is an incremental process
Structure matters
- The same problem can typically be represented in many different forms
- Some of them are better than the others
- Deciphering bad structure is not what you want to do
Interactions with PMs are tough
- Nothing is as evident/unambiguous as demos
- misunderstanding are clearly identifiable
- corrective actions can be planned
- It’s better to demo
- less functionality but actually working
- stuff you know why it’s there
- “When in doubt, leave it out” (Josh Bloch)
- missing things are easy to spot
Functionality vs. Graphics
- Functionality is important to have first
- Graphics is also important but not at first
- investing into it too early is typically waste
- developers are seldom good visual designers
Duplicity is the Root of all Evil
- Similarity is not necessarily duplicity
- Magic numbers and string literals are almost always evil
- in production code you have to use constants
- however, in tests the situation is different
- Repeated code blocks of the same semantics are always evil
Refactoring is hard
- We need to have solid test suite in order to do it safely
- Keeping increments small is tough and non-intuitive
- What you typically do is rewriting instead
Team work is hard
- You’ve typically optimized the amount of code produced
- Bus factor remains low
- In PV168 the thing to optimize is the amount of stuff learned
- In the industry you have to optimize the amount of functionality
- For really difficult parts it’s wise to go in tandem
- “Doctors never operate alone” (Michael Feathers)
- Camera turned on is a must for on-line cooperation
Copy & Paste can be dangerous
- Unless you know what you’re doing
- When using sources such as Stack Overflow, Geeks for Geeks
- you need to understand the difference in context
- adapt the proposal to your specific case
- and get to know the details to be able to defend the solution
Law of the Instrument
If the only tool you have is a hammer, it is tempting to treat
everything as if it were a nail. (Abraham Maslow)
- Especially newly gained tools/techniques tend to be “hammers”
- Not a single thing is ultimately good for everything
- You always need to think in context
Source Code is Text
- Programming by mouse isn’t efficient
- it’s not accurate enough
- it’s not fast enough
- it’s exhausting your mental capacity
- Programming is only a craft
- not a science neither an art
- Craftsmen have good tools and operate them efficiently
- keyboard is the ideal tool for programmers
- IDE is the ideal tool for programmers
Thank you for your hard work!
You’ve learnt a lot about programming already.