This presentation introduces the concept of synchronization beatween threads, as implemented in the Java platform. It is the first part of a series of slides dedicated to thread synchronization. This slides introduces the following concepts:
- Thread safety
- Types of race conditions
- Locking (reentrant locks, intrinsic locks, synchronized blocks)
- Locking pitfalls
The presentation is took from the Java course I run in the bachelor-level informatics curriculum at the University of Padova.
1. CONCURRENT PROGRAMMING
SYNCHRONIZATION (PART 1)
PROGRAMMAZIONE CONCORRENTE E DISTR.
Università degli Studi di Padova
Dipartimento di Matematica
Corso di Laurea in Informatica, A.A. 2015 – 2016
rcardin@math.unipd.it
3. Programmazione concorrente e distribuita
INTRODUCTION
Threads can reduce development cost and
improve the performance application
Exploiting multiple processors
Improved throughput by utilizing available processors
Simplicity of modeling
Use every thread to do a specific task is simplier than using
one thread to do them all
Simplified handling of asynchronous events
Prevention of server’s stall while it is fulfilling a request
More reponsive user interfaces
Use of different threads for GUI and event management
Event dispatch thread (EDT)
3Riccardo Cardin
4. Programmazione concorrente e distribuita
INTRODUCTION
Risks of threads
Safety hazards
Without sufficient synchronization, the ordering of
operations in multiple threads is unpredictable
4Riccardo Cardin
public class UnsafeSequence {
private int value;
/** Returns a unique value. */
public int getNext() {
return value++; // Three operations: read, add and store
}
}
With unlucky timing, two
threads could call
getNext and receive the
same value
5. Programmazione concorrente e distribuita
INTRODUCTION
Risks of threads
Liveness hazards
An activity gets into a state such that it is permanently unable
to make forward progress
Dining philosophers problem
Performance hazards
Poor service time, responsiveness, throughput,
resource consumption, or scalability
Context switch is not cost free
5Riccardo Cardin
If thread A is waiting for a resource that thread B holds
exclusively, and B never releases it, A will wait forever.
7. Programmazione concorrente e distribuita
THREAD SAFETY
Writing thread-safe code is about managing
access to shared and mutable state
Object state is represented by its data
By Shared, we mean that a variable could be
accessed by multiple threads
By mutable, that its value could change
It is far easier to design a class to be thread-safe than
to retrofit it later
7Riccardo Cardin
Whenever more than a thread accesses a given state variable, and one
of them might write to it, they all must coordinate their access to it
using synchronization
-- Brian Goetz
8. Programmazione concorrente e distribuita
THREAD SAFETY
There are three ways to fix a mutable shared
variable
Don’t share the state variable across threads
Make the state variable immutable
Use synchronization whenever accessing it
Object oriented techniques favor thread safety
Encapsulation
Immutability
Clear specification of invariants
8Riccardo Cardin
Stateless objects are always thread-safe.
-- Brian Goetz
10. Programmazione concorrente e distribuita
RACE CONDITIONS
Race conditions
The correctness of a computation depends on
relative timing of multiple threads at runtime
Atomicity
A set of statements is not atomic if they are not executed in a
single, indivisible operation
Read-modifiy-write
Check-then-act
10Riccardo Cardin
A class is thread-safe if it behaves correctly when accessed from
multiple threads, regardless of the scheduling or interleaving of the
execution of those threads by the runtime environment.
-- Brian Goetz
11. Programmazione concorrente e distribuita
RACE CONDITIONS
Read-modify-write race condition
The value of value is read, then modified adding 1
and finally stored into value variable
Among the execution of every statement, control flow could
be preempted by another thread
11Riccardo Cardin
public class UnsafeSequence {
private int value;
/** Returns a unique value. */
public int getNext() {
value = value + 1;
return value;
}
}
read value value + 1 store value
Possible preemption
12. Programmazione concorrente e distribuita
RACE CONDITIONS
Check-then-act race condition
Lazy initialization
The boolean expression depends on a value that is
modified according to it
12Riccardo Cardin
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
// Check-then-act
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
check instance create object store instance
Possible preemption
13. Programmazione concorrente e distribuita
RACE CONDITIONS
Compound actions
Sequences of operations that must be executed
atomically in order to remain thread-safe.
Read-modify-write and Check-then-act must always be
atomic to be thread-safe
Atomicity is relative to operation that are executed
on shared state
The java.util.concurrent.atomic package contains
atomic variable classes for effecting atomic state transitions
13Riccardo Cardin
public class SafeSequence {
private AtomicInteger value;
public int getNext() {
return value.incrementAndGet(); // Atomic read-modify-write
}
}
15. Programmazione concorrente e distribuita
SHARING STATE
Writing correct concurrent programs is primarily
about managing access to shared, mutable state
It’s all about memory visibility
We want to ensure that when a thread modifies the state of
an object, other threads can actually see those changes
If shared state is represented by more than one variable,
atomic classes are not useful
15Riccardo Cardin
public class UnsafeCachedSequence {
private AtomicInteger lastValue;
private AtomicInteger value;
public int getNext() {
// Invariant of the class is not satisfied anymore
lastValue.set(value.get());
return value.incrementAndGet();
}
}
16. Programmazione concorrente e distribuita
LOCKING
To preserve state consistency, update related
state variable in a single atomic operation
Java has a built-in locking mechanism for enforcing
atomicity: synchronized block
But, it is easier to understand the synchronized
keyword after having seen locks in isolation...
16Riccardo Cardin
synchronized (lock) {
// Access or modify shared state guarded by lock
}
Object that will serve
as lock
Block code to be guarded by
the lock
17. Programmazione concorrente e distribuita
LOCKING
Reentrant locking
Use a Lock to protect a code block
The construct guarantees that only one thread at time
can enter the critical section
Always release the lock in a finally block to prevents deadlocks
The class ReentranctLock implements basic functionalities of
a lock
17Riccardo Cardin
myLock.lock(); // a ReentrantLock object
try {
// Operation in this block are executed atomically
} finally {
// make sure the lock is unlocked even if an
// exceptions thrown
myLock.unlock();
}
18. Programmazione concorrente e distribuita
LOCKING
Reentrant locking
The lock acts as mutual exclusion locks
18Riccardo Cardin
public class SafeCachedSequence {
private Lock lock = new ReentrantLock();
private int lastValue;
private int value;
public int getNext() {
// Invariant of the class is now satisfied
lock.lock();
try {
lastValue = value;
value = value + 1;
int result = value;
} finally {
lock.unlock();
}
return result;
}
}
Now the invariant of
the class is
guaranteed by the
lock: value and
lastValue will always
be updated in a
consistent way
20. Programmazione concorrente e distribuita
LOCKING
Reentrant locking
Different threads have to synchronize using the same
instance of the lock
It is called reetrant because a thread can repeatedly
acquire a lock that it already owns
The lock has a hold count that keeps track of the nested calls
to the lock method
Prevents deadlocks wrt the subclass mechanism
Every object in Java (since 1.0) has an intrinsic lock
The synchronized keyword on a method protects the
access to that method using this reference as lock object
All method’s code is guarded
20Riccardo Cardin
21. Programmazione concorrente e distribuita
LOCKING
Intrinsic locking
To call the method, a thread must acquire the
intrinsic object lock
Static synchronized methods use the Class object as lock.
21Riccardo Cardin
public synchronized void method() {
// method body
}
// ...is equivalent to
public void method() {
this.intrinsicLock.lock();
try {
// method body
} finally {
this.intrinsicLock.unlock();
}
}
22. Programmazione concorrente e distribuita
LOCKING
Intrinsic locking suffers of performance issues
Use synchronization by Lock object
Or synchronized block
It uses the reference to an object that will serve as lock
22Riccardo Cardin
public class SafeCachedSequence {
private Object lock = new Object();
private int lastValue;
private int value;
public int getNext() {
synchronized (lock) {
lastValue = value;
value = value + 1;
int result = value;
}
return result;
}
}
24. Programmazione concorrente e distribuita
LOCKING PITFALLS
All the accesses to a mutable shared variable
must be performed with the same lock held
Not only compound actions
Not all data needs to be guarded by a lock
Only mutable data
All the variables involved in the same invariant
must be guarded by the same lock
It is not sufficient to use intrinsic lock on every
method
24Riccardo Cardin
// Not thread-safe
if (!vector.contains(element))
vector.add(element);
25. Programmazione concorrente e distribuita
LOCKING PITFALLS
Poor concurrency
Limits by the availability of processing resources, not
by the structure of application itself
CPU intensive and I/O operations must be outside
synchronized blocks
Acquiring and releasing a lock has some overhead
Not break down synchronized blocks too far
25Riccardo Cardin
There is frequently a tension between simplicity and performance.
When implementing a synchronization policy, resist the temptation to
prematurely sacrifice simplicity (potentially compromising safety) for
the sake of performance.
-- Brian Goetz