Ao (Leo) Li
static int balance = 100;
void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
void main() {
Thread t1 = new Thread(() -> withdraw(100));
Thread t2 = new Thread(() -> withdraw(100));
t1.start();
t2.start();
t1.join();
t2.join();
assert balance >= 0; // can this assert fail?
}
It depends because this is a concurrent program.
a = 0;
b = 0;
a = 1;
r1 = b;
b = 1;
r2 = a;
Possible outcomes: (r1, r2) = (1, 0), (0, 1), (0, 0), and (1, 1).
Each core writes to its local cache/buffer before updating the main memory.
One thread may not see the most recent writes from another thread.
Bonus: will data race happen on a single core machine?
Answer: Yes! https://aoli.al/blogs/data-race/
memory_write_operation(Thread 1, &balance);
balance = 0;
memory_read_operation(Thread 2, &balance);
if (balance > 100) {...}
static AtomicInteger balance = 100;
lock.lock();
if (balance >= amount) {...}
lock.unlock();
a = 0;
b = 0;
r1 = a;
r2 = a;
if (r1 == r2) {
b = 2;
}
r3 = b;
a = r3
Will r1 = r2 = r3 = 2 be possible?
Yes
a = 0;
b = 0;
r1 = x;
if (r1 != 0) {
y = 42;
}
r2 = y;
if (r2 != 0) {
x = 42;
}
Will r1 = r2 = 42 be possible?
No, but why?
Defines how threads in a Java program interact through memory and what behaviors are allowed in concurrent execution.
Small tests to verify specification behaviors or potential bugs.
litmusTest ({
object : LitmusIIOutcome () { var x , y = 0, 0 }
}) {
thread { x = 1; r1 = y }
thread { y = 1; r2 = x }
spec {
accept (0 , 1)
accept (1 , 0)
accept (1 , 1)
interesting (0 , 0)
}
}
static AtomicInteger balance = 100;
void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
withdraw(100):r1 = load(balance) // 100—100—r2 = load(balance) // 100100if (r1 >= 100) store(balance, 100 - 100)—0—if (r2 >= 100) store(balance, 0 - 100)-100The withdraw operation is not atomic.
The desired serializability among multiple memory accesses is violated.
static AtomicInteger balance = 100;
static ReentrantLock lock = new ReentrantLock();
void withdraw(int amount) {
lock.lock();
if (balance >= amount) {
balance -= amount;
}
lock.unlock();
}
void fastWithdraw(int amount) {
int oldBalance = balance;
new Thread(() -> withdraw(amount/2)).start();
new Thread(() -> withdraw(amount - amount/2)).start();
assert(balance == oldBalance - amount); // can this assert fail?
}
old = balance // 100——start(A)ready—start(B)readyreadyassert(balance == old - amount)— (not run yet)— (not run yet)—withdraw(amount/2) // balance: 50———withdraw(amount-amount/2) // balance: 0The assert may observe balance as 100 (or 50).
The desired order between two (groups of) memory accesses is flipped.
void fastWithdraw(int amount) {
int oldBalance = balance;
Thread t1 = new Thread(() -> withdraw(amount/2));
t1.start();
t1.join(); // Blocked until t1 finishes
Thread t2 = new Thread(() -> withdraw(amount - amount/2));
t2.start();
t2.join(); // Blocked until t2 finishes
assert(balance == oldBalance - amount); // can this assert fail?
}
void fastWithdraw(int amount) {
int oldBalance = balance;
new Thread(() -> withdraw(amount/2)).start();
new Thread(() -> withdraw(amount - amount/2)).start();
assert(balance == oldBalance - amount); // can this assert fail?
}
withdraw(amount/2) → withdraw(amount - amount/2) → assertwithdraw(amount - amount/2) → withdraw(amount/2) → assertassert → withdraw(amount/2) → withdraw(amount - amount/2)withdraw(amount/2) → assert → withdraw(amount - amount/2)Concurrency testing: test a concurrent program under different interleavings.
void fastWithdrawTest() {
for (int i = 0; i < 1000; i++) {
balance = 100;
fastWithdraw(100);
}
}
void fastWithdraw(int amount) {
int oldBalance = balance;
new Thread(() -> withdraw(amount/2)).start();
Thread.sleep(1000);
new Thread(() -> withdraw(amount - amount/2)).start();
assert(balance == oldBalance - amount); // can this assert fail?
}
balance = 100;
total_account = 1;
balance=100 → total_account=1 total_account=1 → balance=100
Ao (Leo) Li