Principles of Software Construction:
Objects, Design, and Concurrency
Part 3: Concurrency
Introduction to concurrency
Josh Bloch Charlie Garrod
17-214 1
Administrivia
• Homework 5 team sign-up deadline tomorrow at 5 p.m. EDT
• Midterm exam tomorrow/Thursday (4/7-8)
– Practice exam is out
– Exam review session tonight 7-9 p.m.
– Exam released Wednesday night, due Thursday 11:59 p.m.
17-214 2
Today’s lecture: concurrency motivation and primitives
• Why concurrency?
– Motivation, goals, problems, …
• Concurrency primitives in Java
• Coming soon (not today):
– Higher-level abstractions for concurrency
– Program structure for concurrency
– Frameworks for concurrent computation
17-214 6
Moore’s Law (1965) – number of transistors on a chip
doubles every two years
17-214 7
CPU Performance and Power Consumption
• Dennard Scaling (1974) – each time you double transistor density:
– Speed (frequency) goes up by about 40% (Why 40%?)…
– While power consumption of the chip stays constant (proportional to area)
• Combined w/ Moore’s law, every 4 years the number of transistors
quadruples, speed doubles, and power consumption stays constant
• It was great while it lasted…
– Started breaking down in the mid ’90s and broke down completely in the
mid ’90s due to leakage currents ☹️
– More power is required at higher frequency, generating more heat
– And there’s a limit to how much heat a chip can tolerate
17-214 8
One option: fix the symptom
• Dissipate the heat
17-214 9
One option: fix the symptom
• Better(?): Dissipate the heat with liquid nitrogen
17-214 10
17-214 11
Concurrency then and now
• In the past, multi-threading just a convenient abstraction
– GUI design: event dispatch thread
– Server design: isolate each client's work
– Workflow design: isolate producers and consumers
• Now: required for scalability and performance
17-214 12
We are all concurrent programmers
• Java is inherently multithreaded
• To utilize modern processors, we must write multithreaded code
• Good news: a lot of it is written for you
– Excellent libraries exist (e.g., java.util.concurrent)
• Bad news: you still must understand fundamentals
– …to use libraries effectively
– …to debug programs that make use of them
17-214 13
Aside: Concurrency vs. parallelism, visualized
• Concurrency without parallelism:
Thread1
Thread2
Thread3
• Concurrency with parallelism:
Thread1
Thread2
Thread3
17-214 14
Basic concurrency in Java
Review
• An interface representing a task
public interface Runnable {
void run();
}
• A class to execute a task in a CPU thread
public class Thread {
public Thread(Runnable task);
public void start();
public void join();
…
}
17-214 15
Example: Money-grab (1/2)
public class BankAccount {
private long balance;
public BankAccount(long balance) {
this.balance = balance;
}
static void transferFrom(BankAccount source,
BankAccount dest, long amount) {
source.balance -= amount;
dest.balance += amount;
}
public long balance() {
return balance;
}
}
17-214 16
Example: Money-grab (2/2)
What would you expect this program to print?
public static void main(String[] args) throws InterruptedException {
BankAccount bugs = new BankAccount(100);
BankAccount daffy = new BankAccount(100);
Thread bugsThread = new Thread(()-> {
for (int i = 0; i < 1_000_000; i++)
transferFrom(daffy, bugs, 100);
});
Thread daffyThread = new Thread(()-> {
for (int i = 0; i < 1_000_000; i++)
transferFrom(bugs, daffy, 100);
});
bugsThread.start(); daffyThread.start();
bugsThread.join(); daffyThread.join();
System.out.println(bugs.balance() + daffy.balance());
}
17-214 17
What went wrong?
• Daffy & Bugs threads had a race condition for shared data
– Transfers did not happen in sequence
• Reads and writes interleaved randomly
– Random results ensued
17-214 18
The challenge of concurrency control
• Not enough concurrency control: safety failure
– Incorrect computation
• Too much concurrency control: liveness failure
– Possibly no computation at all (deadlock or livelock)
17-214 19
Shared mutable state requires concurrency control
• Three basic choices:
1. Don't share: isolate state in individual threads
2. Don't mutate: share only immutable state
3. If you must share mutable state: synchronize to achieve safety
17-214 20
An easy fix for our BankAccount program:
public class BankAccount {
private long balance;
public BankAccount(long balance) {
this.balance = balance;
}
static synchronized void transferFrom(BankAccount source,
BankAccount dest, long amount) {
source.balance -= amount;
dest.balance += amount;
}
public long balance() {
return balance;
}
}
17-214 21
Concurrency control with Java’s intrinsic locks
with an explicit lock
• synchronized (lock) { … }
– Synchronizes entire block on object lock; cannot forget to unlock
– Intrinsic locks are exclusive: One thread at a time holds the lock
– Intrinsic locks are reentrant: A thread can repeatedly get same lock
Thread1
Thread2
Thread3
17-214 22
Concurrency control with Java’s intrinsic locks
with an implicit lock
• synchronized (lock) { … }
– Synchronizes entire block on object lock; cannot forget to unlock
– Intrinsic locks are exclusive: One thread at a time holds the lock
– Intrinsic locks are reentrant: A thread can repeatedly get same lock
• synchronized on an instance method
– Equivalent to synchronized (this) { … } for entire method
• synchronized on a static method in class Foo
– Equivalent to synchronized (Foo.class) { … } for entire method
17-214 23
Another example: serial number generation
What would you expect this program to print?
public class SerialNumber {
private static long nextSerialNumber = 0;
public static long generateSerialNumber() {
return nextSerialNumber++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1_000_000; j++)
generateSerialNumber();
});
threads[i].start();
}
for(Thread thread : threads)
thread.join();
System.out.println(generateSerialNumber());
}
}
17-214 24
What went wrong?
• An action is atomic if it is indivisible
– Effectively, it happens all at once
• No effects of the action are visible until it is complete
• No other actions have an effect during the action
• Java’s ++ (increment) operator is not atomic!
– It reads a field, increments value, and writes it back
• If multiple calls to generateSerialNumber see the same
value, they generate duplicates
17-214 25
Again, the fix is easy
public class SerialNumber {
private static long nextSerialNumber = 0;
public static synchronized long generateSerialNumber() {
return nextSerialNumber++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1_000_000; j++)
generateSerialNumber();
});
threads[i].start();
}
for(Thread thread : threads)
thread.join();
System.out.println(generateSerialNumber());
}
}
17-214 26
But you can do better!
java.util.concurrent is your friend
public class SerialNumber {
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException{
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1_000_000; j++)
generateSerialNumber();
});
threads[i].start();
}
for(Thread thread : threads) thread.join();
System.out.println(generateSerialNumber());
}
}
17-214 27
Some actions are atomic
Precondition: Thread A: Thread B:
int i = 7; i = 42; ans = i;
• What are the possible values for ans?
17-214 28
Some actions are atomic
Precondition: Thread A: Thread B:
int i = 7; i = 42; ans = i;
• What are the possible values for ans?
i: 00000…00000111
…
i: 00000…00101010
ans: 00000…00101111
17-214 29
Some actions are atomic
Precondition: Thread A: Thread B:
int i = 7; i = 42; ans = i;
• What are the possible values for ans?
i: 00000…00000111
…
i: 00000…00101010
• In Java:
– Reading an int variable is atomic
– Writing an int variable is atomic
– Thankfully, ans: 00000…00101111 is not possible
17-214 30
Bad news: some simple actions are not atomic
• Consider a single 64-bit long value
high 32 bits low 32 bits
– Concurrently:
• Thread A writing high and low bits
• Thread B reading high and low bits
Precondition: Thread A: Thread B:
long i = 10_000_000_000; i = 42; ans = i;
ans: 01001…00000000 (10,000,000,000)
All are
ans: 00000…00101010 (42) possible!
ans: 01001…00101010 (10,000,000,042)
17-214 31
Yet another example: cooperative thread termination
How long would you expect this program to run?
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws Exception {
Thread backgroundThread = new Thread(() -> {
while (!stopRequested)
/* Do something */ ;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(5);
stopRequested = true;
}
}
17-214 32
What went wrong?
• In the absence of synchronization, there is no guarantee as to
when, if ever, one thread will see changes made by another
• JVMs can and do perform this optimization (“hoisting”):
while (!done)
/* do something */ ;
becomes:
if (!done)
while (true)
/* do something */ ;
17-214 33
Why is synchronization required for communication
among threads?
• Naively: Process
– Thread state shared in memory Thread Thread
Memory
• A (slightly) more accurate view:
– Separate state stored in registers and caches, even if shared
Process
Thread Thread
Cache Cache
Memory
17-214 34
How do you fix it?
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws Exception {
Thread backgroundThread = new Thread(() -> {
while (!stopRequested())
/* Do something */ ;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(5);
requestStop();
}
}
17-214 35
A better(?) solution
volatile is synchronization without mutual exclusion
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws Exception {
Thread backgroundThread = new Thread(() -> {
while (!stopRequested)
/* Do something */ ;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(5);
stopRequested = true;
}
}
17-214 36
Summary
• Like it or not, you’re a concurrent programmer
• Ideally, avoid shared mutable state
– If you can’t avoid it, synchronize properly
• Some things that look atomic aren’t (e.g., val++)
• Even atomic operations require synchronization
– e.g., stopRequested = true
– Synchronization is required for communication as well as mutual exclusion
17-214 37