The Java Memory Model defines how threads interact through memory.
All source code is available at github.
Computer Memory Model
sh-4.2# lscpu | grep 'CPU(s)\|cache'
CPU(s): 2
On-line CPU(s) list: 0,1
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 4096K
sh-4.2# cat /proc/meminfo | grep MemTotal
MemTotal: 2047036 kB
Computer Memory Model.png
- 2 threads(1 thread per cpu) may run concurrently.
- Data is read from RAM to Cache, then processed by CPU, then written(flushed) to the RAM.
- The CPU Optimizer determines when to flush.
- Data in the Cache is NOT visible to other threads.
- Data in the RAM is visible to all threads, threads can communicate through RAM.
JMM goals
JMM need to ensure visibility, ordering and atomicity regardless of the underlying hardware memory model.
A basic understanding of java concurrent stress test(JCST) is required.
You can check the sample code from github.
Visibility
Write by one thread to a shared object may be invisible to another thread because of the existence cache.
Code
- Thread actor1 keeps reading from shared object (
v
). - Thread actor1 exits while
v = 1
(changes from Thread signal is visible). - Thread signal is started after Thread actor1.
- Thread signal write(
v = 1
) to shared object.
@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE, desc = "write in signal is visible to actor1.")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc = "write in signal is NOT visible to actor1.")
public class VisibilityTest {
@JCStressTest(Mode.Termination)
@JCStressMeta(VisibilityTest.class)
@State
public static class PlainReadWrite {
int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}
@JCStressTest(Mode.Termination)
@JCStressMeta(VisibilityTest.class)
@State
public static class VolatileReadWrite {
volatile int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}
@JCStressTest(Mode.Termination)
@JCStressMeta(VisibilityTest.class)
@State
public static class SynchronizedReadWrite {
int v;
@Actor
public void actor1() {
while (v == 0) {
synchronized (this) {
if (v == 0) {
continue;//release lock
}
}
}
}
@Signal
public void signal() {
synchronized (this) {
v = 1;
}
}
}
}
Result
- JCST TERMINATED state means write in Thread signal is visible to Thread actor1.
- JCST STALE state means write in Thread signal is NOT visible to Thread actor1.
Conclusion
- PlainReadWrite
- Visibility of plain access is NOT ensured.
- VolatileReadWrite
-
volatile
ensures visibility of shared object.
-
- SynchronizedReadWrite
-
synchronized
ensures visibility of shared object.
-
Ordering
- The compiler is free to reorder certain instructions as an optimization when it would not change the semantics of the program.
- The processor is allowed to execute operations out of order under some circumstances.
- The cache is generally allowed to write variables back to main memory in a different order than they were written by the program.
Code
- Thread actor1 write to
i = 1; iSet = true;
. - Thread actor2 read from
r.r1 = iSet; r.r2 = i;
.
@Outcome(id = "false, 0", expect = ACCEPTABLE, desc = "actor2 reads before actor1.")
@Outcome(id = "true, 1", expect = ACCEPTABLE, desc = "actor2 reads after actor1.")
@Outcome(id = "false, 1", expect = ACCEPTABLE, desc = "actor1 write i, actor2 read iSet, actor2 read i, actor1 write iSet")
@Outcome(id = "true, 0", expect = ACCEPTABLE_INTERESTING, desc = "REORDERING, actor1 write iSet, actor2 read iSet, actor2 read i, actor1 write i")
public class OrderingTest {
@JCStressTest
@JCStressMeta(OrderingTest.class)
@State
public static class PlainOrdering {
int i;
boolean iSet;
@Actor
public void actor1() {
i = 1;
iSet = true;
}
@Actor
public void actor2(ZI_Result r) {
r.r1 = iSet;
r.r2 = i;
}
}
@JCStressTest
@JCStressMeta(OrderingTest.class)
@State
public static class VolatileOrdering {
int i;
volatile boolean iSet;
@Actor
public void actor1() {
i = 1;
iSet = true;
}
@Actor
public void actor2(ZI_Result r) {
r.r1 = iSet;
r.r2 = i;
}
}
@JCStressTest
@JCStressMeta(OrderingTest.class)
@State
public static class SynchronizedOrdering {
int i;
boolean iSet;
@Actor
public void actor1() {
synchronized (this) {
i = 1;
iSet = true;
}
}
@Actor
public void actor2(ZI_Result r) {
synchronized (this) {
r.r1 = iSet;
r.r2 = i;
}
}
}
}
Result
Execution Order.png- JCST (false, 0) state, read write is executed as case 1.
- JCST (false, 1) state, read write is executed as case 2.
- JCST (true, 0) state, read write is executed as case 4/5(reordering observed).
- JCST (true, 1) state, read write is executed as case 3.
Conclusion
- PlainOrdering
- Ordering of plain write and read is NOT ensured.
- VolatileOrdering
-
volatile
ensures ordering of write and read.
-
- SynchronizedOrdering
-
synchronized
ensures ordering of write and read.
-
Atomicity
An operation is atomic,
A set of operations is atomic,
Code
public class AtomicityTest {
@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "See initial value while writer not finished.")
@Outcome(id = "-1", expect = Expect.ACCEPTABLE, desc = "See full value while writer finished")
@Outcome(expect = Expect.FORBIDDEN, desc = "partial values are forbidden.")
@State
public static class IntegerAtomicity {
int v;
@Actor
public void writer() {
v = 0xFFFFFFFF;
}
@Actor
public void reader(I_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "See initial value while writer not finished.")
@Outcome(id = "65535", expect = Expect.ACCEPTABLE, desc = "See full value while writer0 finished.")
@Outcome(id = "-65536", expect = Expect.ACCEPTABLE, desc = "See full value while writer1 finished.")
@Outcome(expect = Expect.FORBIDDEN, desc = "Partial values are forbidden even in case of concurrent update.")
@State
public static class IntegerConcurrentAtomicity {
int v;
@Actor
public void writer0() {
v = 0x0000FFFF;
}
@Actor
public void writer1() {
v = 0xFFFF0000;
}
@Actor
public void reader(I_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "See initial value while writer not finished.")
@Outcome(id = "-1", expect = Expect.ACCEPTABLE, desc = "See full value while writer finished.")
@Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "Partial values violate access atomicity, but allowed under JLS.")
@State
public static class LongAtomicity {
long v;
@Actor
public void writer() {
v = 0xFFFFFFFF_FFFFFFFFL;
}
@Actor
public void reader(J_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "See initial value while writer not finished.")
@Outcome(id = "-1", expect = Expect.ACCEPTABLE, desc = "See full value while writer finished.")
@Outcome(expect = Expect.FORBIDDEN, desc = "Partial values are forbidden.")
@State
public static class VolatileLongAtomicity {
volatile long v;
@Actor
public void writer() {
v = 0xFFFFFFFF_FFFFFFFFL;
}
@Actor
public void reader(J_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "One update lost.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Both updates.")
@State
public static class PlainIncrement {
int v;
@Actor
public void actor1() {
v++;
}
@Actor
public void actor2() {
v++;
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "One update lost.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Both updates.")
@State
public static class VolatileIncrement {
volatile int v;
@Actor
public void actor1() {
v++;
}
@Actor
public void actor2() {
v++;
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "1", expect = Expect.FORBIDDEN, desc = "One update lost.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Both updates.")
@State
public static class SynchronizedIncrement {
int v;
@Actor
public void actor1() {
synchronized (this) {
v++;
}
}
@Actor
public void actor2() {
synchronized (this) {
v++;
}
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = v;
}
}
@JCStressTest
@Outcome(id = "1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "One update lost.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Both updates.")
@State
public static class AtomicIntegerIncrement {
AtomicInteger v = new AtomicInteger();
@Actor
public void actor1() {
v.getAndIncrement();
}
@Actor
public void actor2() {
v.getAndIncrement();
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = v.get();
}
}
@JCStressTest
@Outcome(id = "100, 0", expect = Expect.ACCEPTABLE, desc = "Transfer not start yet.")
@Outcome(id = "0, 100", expect = Expect.ACCEPTABLE, desc = "Transfer completed.")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "See partial result.")
@Outcome(id = "100, 100", expect = Expect.ACCEPTABLE_INTERESTING, desc = "See partial result.")
@State
public static class PlainTransfer {
int a = 100;
int b;
@Actor
public void actor1() {
a = a - 100;
b = b + 100;
}
@Actor
public void arbiter(II_Result r) {
r.r1 = a;
r.r2 = b;
}
}
@JCStressTest
@Outcome(id = "100, 0", expect = Expect.ACCEPTABLE, desc = "Transfer not start yet.")
@Outcome(id = "0, 100", expect = Expect.ACCEPTABLE, desc = "Transfer completed.")
@Outcome(expect = Expect.FORBIDDEN, desc = "Forbidden case.")
@State
public static class SynchronizedTransfer {
int a = 100;
int b;
@Actor
public void actor1() {
synchronized (this) {
a = a - 100;
b = b + 100;
}
}
@Actor
public void arbiter(II_Result r) {
synchronized (this) {
r.r1 = a;
r.r2 = b;
}
}
}
}
Result
- Integer Atomicity
- Long Atomicity
- Increment Atomicity
- Transfer Atomicity
Conclusion
-
Read/Write of primitive types (boolean, byte, char, short, int, float, double, long) is atomic.
Concurrent update of primitive types is safe. -
Read/Write of long/double in 32 bits machine is not atomic.
R/W to long/double can be divided into R/W high 32bits and low 32bits.
volatile long/double
ensures atomicity jls. -
Increment(
i++
) is not atomic.
i++
can be divided to (load i, i+1, store i).
volatile
doesn't ensure atomicity.
java.util.concurrent.atomic.AtomicInteger
ensures atomicity.
synchronized
ensures atomicity. -
Transfer $100 from A to B is not atomic.
This can be divided to (A-100, B+100).
synchronized (A-100, B+100)
ensures atomicity.
Memory Barrier
Types of memory barrier
Memory Barrier.pngJSR-133 ordering rules
JSR-133 ordering rules.png- Volatile Load can't be reordered with subsequent operations.
- Monitor Enter can't be reordered with subsequent operations.
- Volatile Store can't be reordered with precedent operations.
- Monitor Exit can't be reordered with precedent operations.
- Volatile Store can't be reordered with subsequent Volatile Load and Monitor Enter.
- Monitor Exit can't be reordered with subsequent Volatile Load and Monitor Enter.
volatile
volatile memory barrier.pngsynchronized
synchronized memory barrier.pngfinal
final memory barrier.pngReference
Wiki java memory model
The Java Memory Model by Doug Lea
JLS-17
JSR-133
Fixing the Java Memory Model, Part 1
Fixing the Java Memory Model, Part 2
tutorials jenkov java-memory-model
深入理解Java内存模型
网友评论