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.