- Thread interference demo
- Synchronized methods and statements
- Deadlock
- wait, notify, notifyAll
- Java Lock objects
Thread interference occurs when two operations, running in different threads, but acting on the same data, interleave. These types of issues are difficult to predict.
In concurrent programming, concurrent thread accesses to shared data can lead to unexpected or erroneous behaviour. The part of the program where the shared data is accessed needs to be protected in ways that avoid concurrent access.
This protected section is the critical section or critical region.
An atomic action is required in a critical section where only one thread can execute its critical section at a time. All the other threads have to wait to access the critical section.
Thread interference demo
Consider the following class Counter
that simply keeps track of a count that can be incremented and decremented.
For the purposes of illustration, the increment and decrement are done in explicitly separated steps (as opposed to count++
and count--
), and they include some Thread.sleep
calls so that there is a higher possibility of threads interleaving.
public class Counter {
private int count = 0;
public void increment() {
try {
int val = this.count;
Thread.sleep(1000);
int newVal = val + 1;
Thread.sleep(1000);
this.count = newVal;
} catch (InterruptedException e) {
// no-op
}
}
public void decrement() {
try {
int val = this.count;
Thread.sleep(1000);
int newVal = val - 1;
Thread.sleep(1000);
this.count = newVal;
} catch (InterruptedException e) {
// no-op
}
}
public int getCount() {
return this.count;
}
}
As described above, if two threads both run the increment
method on the same object, depending on how the threads get scheduled, there is a possibility of the work of one thread being overwritten by another thread.
Counter counter = new Counter();
Thread t1 = new Thread(() -> counter.increment());
Thread t2 = new Thread(() -> counter.increment());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
Suppose Thread t1
runs at about the same time as Thread t2
. If the initial value of count
is 0, this sequence can happen:
- Thread A: Retrieve count.
- Thread B: Retrieve count.
- Thread A: Increment retrieved value; result is 1.
- Thread B: Increment retrieved value; result is 1.
- Thread A: Store result in count; count is now 1.
- Thread B: Store result in count; count is now 1.
Thread A’s result is lost, overwritten by Thread B (the program will print 1
). Under different circumstances, it might be Thread B’s result that gets lost (program prints 1
), or there could be no error at all (program prints 2
).
To prevent these kinds of “race conditions” (where the result depends on which thread wins a race), we need some way to “synchronize” the execution of the two threads, such that they access and modify shared data in correct ways.
Synchronized methods and statements
Every Object in Java has an internal entity associated with it called its intrinsic lock or monitor lock. When a thread needs exclusive access to an object’s fields or methods, it must acquire the object’s intrinsic lock before accessing them, and release the lock after it’s finished with the object.
When one thread has an object’s intrinsic lock, no other thread can acquire the same lock. The other thread will “block” or suspend its execution until the object’s lock becomes available again.
This notion of an Object lock is extremely useful because it lets us synchronize access to data that is shared by multiple threads (e.g., mutable objects). The critical section mentioned above is the part of the program that is executed between acquiring and releasing an object’s intrinsic lock.
The Java language provides two basic idioms for synchronising access to critical sections: synchronized
methods and synchronized
statements.
Synchronized methods
To make a method synchronized
, simply add the synchronized
keyword to its declaration.
// rest of the Counter class stays the same
public synchronized void increment() {
// ... method stays the same
}
public synchronized void decrement() {
// ... methods stays the same
}
When one thread is executing a synchronized
instance method for an object, all other threads that invoke synchronized
methods for the same object will block (suspend execution) until the first thread is done with the object.
When a thread invokes a synchronized
method, it automatically acquires the intrinsic lock for that method’s object and releases it when the method returns. The lock release occurs even if the return was caused by an uncaught exception.
So in the Counter
example, when Thread t1
is executing on the counter
object, it “owns” the counter
object’s intrinsic lock. So when Thread t2
is kicked off, it requests access to the object, but cannot proceed until Thread t1
releases the lock (i.e., finishes executing the synchronized
method increment
).
In Java synchronization, if a thread wants to enter a synchronized
method it will acquire lock on all synchronized
methods of that object, not just on one synchronized
method that thread is using.
So in the Counter
example, if Thread t1
was calling increment
, and Thread t2
wanted to call decrement
, it still would NOT be able to do so, because it’s a common lock on the counter
object that controls access to all synchronized
methods.
static synchronized
methods do not use an intrinsic lock associated with any instance of the class. Instead, they use lock associated to the Class
object loaded by the JVM. So, one lock for the class, and individual locks for individual instances of the class.
Synchronized statements
Deciding which methods to mark as synchronized
or not can be challenging. In general, you need to identify the critical section, and that critical section needs to take place as an atomic action. That is, it must run through the entire sequence of instructions without another thread being interleaved that accesses the same data.
In many cases, this critical section might be an entire method. However, overly stringent locking can start to erode the benefit you would get from concurrent programming. More often you want finer-grained control over object locking.
Java therefore lets you lock statements or blocks of code instead of entire methods.
synchronized
methods inherently have objects on which to lock (the this
object in the case of instance methods, and the Class
object in the case of static
methods). Byt synchronized
statements have no such object.
Therefore, you must manually provide a synchronized
statement an object on which to lock.
The syntax is similar:
synchronized(this) {
count++;
}
The code above, instead of locking an entire method, locks only the code within the curly braces. In the example above the synchronization is done on the basis of the this
object, just like synchronized
instance methods.
Any object can be used for the lock. For example, within the same class, you may have multiple bits of shared data (c1
and c2
in the example below) that need to be synchronized across threads.
However, we only care that two threads don’t access c1
at the same time, or c2
at the same time. But we don’t care if one thread accesses c1
while another accesses c2
in parallel.
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
Use this idiom (of multiple locks) with extreme care. You must be absolutely sure that c1
and c2
are totally separate and don’t need to be synchronized together.
Deadlock
Sometimes you can get into a situation where two threads are both waiting for each other to terminate. This results in a situation called deadlock, where neither thread makes any progress.
Consider this example from the Java tutorials on concurrency.
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s"
+ " has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s"
+ " has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new Runnable() {
public void run() { alphonse.bow(gaston); }
}).start();
new Thread(new Runnable() {
public void run() { gaston.bow(alphonse); }
}).start();
}
}
Alphonse and Gaston are two super polite friends who bow to each other when they meet. They remain bowed until the other has bowed back to them. In most cases, this works out fine. However, if both Alphonse and Gaston bow to each other at the same time, they can never exit their bow, because they are both waiting for the other one to bow back.
Each Friend
object calls its synchronized
bow
method to bow to the other Friend
object. The bow
method implementation calls another synchronized
method, bowBack
on the object that is passed in.
So when Alphonse bows to Gaston, the lock on Alphonse is acquired by the first thread. When Gaston bows to Alphonse, the lock on Gaston is acquired by the second thread. Now Alphonse’s thread needs access to Gaston’s lock in order to make Gaston bowBack
, and Gaston’s thread needs Alphonse’s lock to make Alphonse bowBack
. Neither can progress. Legend has it they are still bowed to each other and must be fed and changed by passers by.
The lesson here is: be careful about calling other objects’ synchronized
methods from a synchronized
method. synchronized
blocks of statements can help you limit which parts of a method must be synchronized
instead of synchronizing the whole method.
Avoiding deadlock
- Avoid Nested Locks – this is the main reason for a deadlock condition.
- Avoid Unnecessary Locks – The locks should be given to the important threads. Giving locks to unnecessary threads can cause the deadlock condition.
- Use
Lock
objects to break deadlocks — The JavaLock
interface represents a concurrent lock which can be used to guard against race conditions inside critical sections. I’ll explain this more after a quick detour.
wait, notify, notifyAll
Sometimes, we need more fine-grained conditional control over how we enter and exit critical sections.
The Object class in Java contains three final methods that allows threads to communicate about the lock status of a resource.
wait
method waits indefinitely for any other thread to callnotify
ornotifyAll
method on the object to wake up the current threadnotify
method wakes up only one thread waiting on the object and that thread starts execution. The choice of the thread to wake depends on the OS implementation of thread management.notifyAll
method wakes up all the threads waiting on the object, although which one will process first depends on the OS implementation.
The above methods can be called on an object to cause the current thread to pause or to wake up. They are commonly used when you want a thread to conditionally wait or execute some code.
For example, consider the BlockingQueue
data structure. It functions more or less like a regular queue, except that:
- When adding something to the queue, it waits for the queue to be below capacity, so that the new item can be accepted
- When something is removed from the queue, it waits for there to be at least one item in the queue before removing and returning it
- When something is removed from the queue, it notifies the object that it’s ready to accept new items (freeing up the
add
method from its waiting)
An example add and remove might look like this (some of the surrounding code has been elided):
public synchronized void add(T element) {
while (this.queue.size() == capacity) {
wait(); // while the queue is at capacity, this request will wait
}
this.queue.add(element); // once the wait is over, add the element
notify(); // signal to the object; use notifyAll to notify all waiting threads
}
public synchronized T remove() {
while (this.queue.isEmpty()) {
wait(); // while the queue is empty, wait
}
T item = this.queue.remove(); // when the wait is over, remove the first item
notify(); // notifyAll to notify all waiting threads
return item;
}
In the add
method, the wait
is waiting for the queue to become below capacity, so something can be added. And the notify
is signalling that there is something in the queue (so the remove
method can do its thing).
In the remove
method, the wait
is waiting for the queue to not be empty, so something can be removed. And the notify
is signalling that something was removed, so that the add
method can do its thing.
So there are TWO “waiting conditions” and TWO signals being sent, but all of it uses the same primitive wait
and notify
mechanism. It’s up to the programmer to manage the different states that need to wait and notify according to various conditions. Using wait
and notify
correctly in a complex module is notoriously difficult for this reason.
Java Lock objects
The synchronized
keyword allows a really simple kind of lock, where access to critical sections of an object’s code can be restricted to one thread at a time. And the wait
/notify
mechanism allows conditional locking and unlocking of certain portions of the synchronized object’s code.
More recent Java versions include higher-level abstractions over these constructs in the form of the Lock
interface. The simplest implementation of this interface (and the one that we’ll talk about) is the ReentrantLock
. The ReentrantLock
provides the same basic behaviour as the synchronized
keyword, with some extended capabilities.
Considering the Counter
example above, we can rewrite the increment
method using the ReentrantLock
like below. Notice that we don’t need to declare the method as synchronized
; we’re kind of doing the equivalent of a synchronized
block here.
public void increment() {
lock.lock();
try {
int val = this.count;
Thread.sleep(1000);
int newVal = val + 1;
Thread.sleep(1000);
this.count = newVal;
} catch (InterruptedException e) {
// no-op
}
lock.unlock();
}
Ok, so the ReentrantLock
provides the same functionality as the synchronized
block. What’s the big deal?
The ReentrantLock
also has the trylock
method, which has the following signature: boolean trylock(long timeout, TimeUnit timeUnit)
It attempts to obtain a lock, but gives up after a specified amount of time. It returns a boolean
value telling us if the lock was successfully obtained or not. This ability to “back out” of trying to obtain a lock helps us to avoid deadlock situations.
Go back to our Alphonse and Gaston example, and consider how you would use this newfound ability to break them out of their lifelong bows. (Or, read the implemented example here.)