Posted on :: Updated on :: 207 Words :: Tags:

General Structure

In order to keep safety and have threads waiting without locking each other out we can have the following structure in general:

//PRE for act: P (z.B. a > b)
public void m(){
    synchronized(lock) {
        while(!P) {
            try { lock.wait(); }
            catch {InterruptedException i} {}
        }
        // P holds here!
        doStuff();
        lock.notifyAll(); //only if P was changed by doStuff()
    }
}

public void setA(int a) {
    synchronized(lock) {
        //update a
        lock.notifyAll();
    }
}

Queue Example

In order to implement a non-blocking queue with different 'normal' locks, we run into a classic problem of nested synchronization on different locks:

public synchronized void enqueue(Object c) { //1
    while ((tail + 1) % SIZE == head) {
        synchronized (notFull) { //2
            try { notFull.wait(); } 
            catch (Exception e) {}
        }
    }

    synchronized (notEmpty) { notEmpty.notify(); } //3
    buf[tail] = c;
    tail = (tail + 1) % SIZE;
}

The code uses nested synchronized blocks on notFull and notEmpty. This can lead to deadlocks if two threads acquire these locks in different orders. For example:

  • Thread A acquires the lock on notFull and waits for notEmpty.
  • Thread B acquires the lock on notEmpty and waits for notFull.
  • Both threads are now stuck, leading to a deadlock.

Much better to use Locks and Conditions:

public class Queue2 {
    private final static int SIZE = 10;
    private final Object[] buf = new Object[SIZE];
    private int tail = 0, head = 0;

    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    public Object dequeue() {
        lock.lock();
        try {
            while (tail == head) { // while empty
                try { notEmpty.await(); } catch (Exception e) {}
            }
            Object e = buf[head]; head = (head + 1) % SIZE;
            notFull.signal();
            return e;
        } finally { lock.unlock(); }
    }
    public void enqueue(Object c) {
        lock.lock();
        try {
            while ((tail + 1) % SIZE == head) {
                try { notFull.await(); } catch (Exception e) {}
            }
            buf[tail] = c; tail = (tail + 1) % SIZE;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
}

The Condition-based implementation in the Queue2 example is superior because:

  • No Nested Locks: It uses a single ReentrantLock with separate Condition objects (notEmpty and notFull), avoiding deadlocks.
  • Fine-Grained Control: Condition objects allow precise signaling (await() and signal()), improving concurrency.
  • Clearer Semantics: The use of Condition objects makes the code easier to understand and maintain.
  • Handles Spurious Wakeups: The while loop ensures the condition is re-checked after waking up.
  • By using ReentrantLock and Condition, the implementation avoids the pitfalls of the synchronized approach and provides a more robust and scalable solution.