Sincronization
Monitors (intrinsic lock)
In Java, synchronization is done using monitors. A monitor is any object that can have a single thread owner. Any thread can claim ownership of a monitor and in return access a critical section of restricted code. If there is already an owner, it must be waited until it ceases to be so.
To request ownership of a monitor and access to the critical code section, we can use the "synchronized" reserved word. Depending on where we do it, the monitor object changes:
- instance methods: The monitor object is the instance. So only one thread per instance.
- class methods: The monitor object is the class. So only one thread per class.
- Blocks of code: the monitor object must be given inside the parentheses. Any object can be a monitor (eg
new Object()
), although we usually make the monitor the same object we want to exercise access control over.
An example of instance methods:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
Consequences:
- First, it is not possible for two threads to call two synchronized methods simultaneously. Subsequent calls are suspended until the first thread finishes with the object.
- Second, when a synchronized method finishes, it establishes a happens-before relationship: subsequent calls will have the changes made visible.
Important: Within a synchronized block, you need to do the minimum possible work: read the data and if necessary, transform it.
An example of code blocks:
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++;
}
}
}
This method allows for finer grain: there can be one thread in lock1's code area and another in lock2's.
Wait / Notify (guarded lock)
Let's imagine that we want to wait until a condition is met:
public void waitForHappiness() {
// Simple method, wastes CPU: never do that!
while (!happiness) {}
System.out.println("Happiness achieved!");
}
We can do this between threads using the classic wait and notify communication method, which allows:
- Wait until a condition involving shared data is true, i
- notify other threads that the shared data has changed, probably triggering a condition that other threads wait for.
The methods are:
- wait(): when called, the current thread waits until another thread calls notify() or notifyall() on this monitor.
- notify(): Wake up any thread of all the ones waiting on this monitor.
- notifyAll(): Wake up all threads that are waiting on this monitor.
The wait() and notify methods must be called from within a synchronized block for the monitor object.
Also, as discussed in Object, the method wait() must be inside a loop waiting for a condition:
// in one thread:
synchronized (monitor) {
while (!condition) {
monitor.wait();
}
}
// in the other thread:
synchronized (monitor) {
monitor.notify();
}
In our case:
synchronized (monitor) {
while (!happiness) {
monitor.wait();
}
}
...
synchronized (monitor) {
happiness = true;
monitor.notify();
}
Operation of wait / notify (high level)
Operation of wait / notify (low level)
Reentrant Lock
Using synchronized is very simple and sufficient in many scenarios. But there is an implementation, ReentrantLock, that allows the following additional features:
- Lock attempt: allows you to try to lock a lock without having to wait.
- Fair locks: allow threads to finish executing in the order in which they requested the lock.
- Conditional locks: allow a thread to wait until a condition is met.
- Locks with interruption: allow a thread to wait until a condition is met, but can be interrupted.
static class MyRunnable implements Runnable {
SharedObject sound;
Lock lock;
MyRunnable(SharedObject so, Lock lock) {
this.sound = sound;
this.lock = lock;
}
void increment() {
try {
lock.lock();
so.counter ++;
} finally {
lock.unlock();
}
}
@Override
public void run() {
for (int i=0; i<1_000_000; i++) {
increment();
}
}
}
As you can see, the increment() method uses a lock to synchronize access to the shared variable. This is the same as using a synchronized method, but with the difference that we can use the lock in a block of code that can throw an exception.