Lecture 19 Java Synchronization

Joseph Haugh

University of New Mexico

Code Examples

Code from today can be found here

Concurrency Issues

  • The performance benefits of concurrency come with added programming complexity.
  • Without the proper use of mutual exclusion when accessing shared resources, some concurrency issues can arise:
    • Thread interference (race conditions)
    • Deadlock
    • Livelock
    • Starvation

Recall: Mutual Exclusion

Mutual exclusion is the requirement that no more than one thread of execution may access a particular critical section at once.

x = 1

Thread A

x = x + 1

Thread B

x = x - 2

What does x equal?

Without mutual exclusion, we don’t know which thread will access, manipulate, and store the result first!

Example of Deadlock: Dining Philosophers

  • 5 philosophers are seated around a table spending their time thinking and eating
  • Each philosopher has a fork to their left and to their right
  • In order to eat a philosopher must have both forks in their hands

Synchronization: Programming Mutual Exclusion

Synchronization is the programming construct used to ensure mutual exclusion in Java.

Two synchronization idioms are used:

Synchronized methods

public synchronized void foo() {
  // ...
}
  • Only one thread at a time can execute in a synchronized method.
  • The lock is on the object providing the methods.

Synchronized blocks

public void foo() {
    synchronized(lock) {
        // ...
    }
}
  • Only the thread possessing the lock can execute in a synchronized block.

Example: Counter

Consider a static Counter which allows two Farmers to keep track of the number of sheep in the pen.

public class Counter {
    private static int n = 0;
    public static void increment() {
        n++;
    }
    public static void decrement() {
        n--;
    }
    public static void printCount() {
        // print sheep count...
    }
}
public class Farmer implements Runnable {
    @Override
    public void run() {
        // two sheep arrive
        Counter.increment();
        Counter.increment();

        // one sheep leaves
        Counter.decrement();
    }
}

How many sheep are in the pen?

Counter: Console Output

public static void
    main(String[] args) {

    Farmer f1 = new Farmer();
    Thread t1 = new Thread(f1);

    Farmer f2 = new Farmer();
    Thread t2 = new Thread(f2);

    t1.start();
    t2.start();

    Counter.printCount();
}

Output

Run 1 -> 2 sheep in the pen.

Run 2 -> 3 sheep in the pen.

Run 3 -> 2 sheep in the pen.

Run 4 -> 1 sheep in the pen.

We have a race condition.

Access to the critical region (where we manipulate the shared resource n) should be synchronized.

Counter: Synchronized Methods

In this trivial example, synchronizing access to both methods eliminates one of the race conditions. What’s the other one?

public class Counter {
    private static int n = 0;

    public static synchronized void increment() {
        n++;
    }

    public static synchronized void decrement() {
        n--;
    }
}

Main: Join On Thread

We also need to join on the threads before counting!

public static void main(String[] args) {
    Farmer f1 = new Farmer();
    Thread t1 = new Thread(f1);

    Farmer f2 = new Farmer();
    Thread t2 = new Thread(f2);

    t1.start();
    t2.start();
    t1.join();
    t2.join();

    Counter.printCount();
}

Output

Run 1 -> 2 sheep in the pen.

Run 2 -> 2 sheep in the pen.

Run 3 -> 2 sheep in the pen.

Run 4 -> 2 sheep in the pen.

No more race condition!

Case Study: Producer/Consumer problem

  • Consider a queue which buffers data provided by a Producer and removed by a Consumer
    • the queue has a maximum size
    • the Producer should not add to a full queue
    • the Consumer should not consume from an empty queue
  • How do we avoid…
    • putting objects in a full queue?
    • attempting to remove them from an empty one?
  • We will explore a solution that implements a singleton queue.
    • Educational purposes only!
    • BlockingQueue implementations already exist!

Producer/Consumer: Design Overview

  • Producer (thread)
    • integers are placed in the Queue
    • waits when the Queue is full
  • Queue – a singleton queue
    • holds integer values
    • starts being empty
    • has a maximum size
  • Consumer (thread)
    • waits for a non-empty Queue
    • consumes its contents

Queue: recall the singleton pattern…

  • A single instance of Queue is referenced globally by the Producer and Consumer
    • thus, we can implement it using the singleton pattern.
  • Recall that the singleton pattern is not necessarily thread-safe unless implemented correctly.
    • lazy vs. eager instantiation
  • Let’s see why…

Queue: Lazy Instantiation

public class Queue {
    private static final int CAPACITY = 5;
    private List<Integer> dataQueue = new ArrayList<>();
    private static Queue uniqueInstance;

    private Queue() {}

    public static Queue getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Queue();
        }
        return uniqueInstance;
    }

    // enqueue(), dequeue()...
}

Queue: Lazy Instantiation Pitfall

Consider the case in which the Producer and Consumer running concurrently in separate threads call Queue.getInstance() in order to access data in the queue…

public static Queue getInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Queue();
    }
    return uniqueInstance;
}

If Consumer reaches the if check before Producer has instantiated uniqueInstance on the assignment line, getInstance() will return two separate, unique instances of Queue!

Queue: Eager Instantiation Fix

public class Queue {
    private static final int CAPACITY = 5;
    private List<Integer> dataQueue = new ArrayList<>();
    private static Queue uniqueInstance = new Queue();

    private Queue() {}

    public static Queue getInstance() {
        return uniqueInstance;
    }

    // enqueue(), dequeue()...
}

uniqueInstance is instantiated eagerly (i.e. before we know we need it). What is another way of addressing this thread-safety issue?

Queue: Synchronized getInstance Fix

public class Queue {
    private static final int CAPACITY = 5;
    private List<Integer> dataQueue = new ArrayList<>();
    private static Queue uniqueInstance;

    private Queue() {}

    public static synchronized Queue getInstance() {
        if (uniqueInstance == null) {
        uniqueInstance = new Queue();
        }
        return uniqueInstance;
    }
    // enqueue(), dequeue()...
}

Here, we preserve lazy instantiation but synchronize access to getInstance(), ensuring that only one thread will be active in the method at a time.

Producer/Consumer Concept

  • In this example (and often in practice), Producers and Consumers are tasks running concurrently in different threads while exchanging information through a shared data structure.

  • Their execution is coordinated using synchronized methods accessed within the Queue object.

Producer

public class Producer implements Runnable {
    @Override
    public void run() {
        Queue queue = Queue.getInstance();

        while (!Thread.interrupted()) {
            Integer newData = new Random().nextInt();
            queue.enqueue(newData);
        }
    }
}

Consumer

public class Consumer implements Runnable {
    @Override
    public void run() {
        Queue queue = Queue.getInstance();

        while (!Thread.interrupted()) {
            // consume the last value in the queue
            queue.dequeue();
        }
    }
}

Producer/Consumer: Guarded Blocks

  • While the Producer and Consumer wait for the Queue to be in an appropriate state (non-full or non-empty, respectively), they must perform guarded blocks.

  • Guarded blocks allow the execution of threads to be coordinated based upon the state of shared variables.

  • There are two types of guarded blocks:

    • Bad: Busy waiting
    • Good: Wait/notify

Queue: Guarded block with a busy wait

public class Queue {
    private static final int CAPACITY = 5;
    private List<Integer> dataQueue = new ArrayList<>();
    private static Queue uniqueInstance;

    // constructor, getInstance() ...

    public void enqueue(Integer data) {
        while (dataQueue.size() >= CAPACITY) {
            // wait...
        }
        dataQueue.add(data);
    }
}

Busy waits are a waste of processor resources!

Queue: Guarded block with wait/notify

public class Queue {
    private static final int CAPACITY = 5;
    private List<Integer> dataQueue = new ArrayList<>();
    private static Queue uniqueInstance;

    // constructor, getInstance() ...

    public synchronized void enqueue(Integer data) {
        while (dataQueue.size() >= CAPACITY) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        dataQueue.add(data);
        notifyAll();
    }
}

What’s happening here?

  • enqueue is synchronized now. wait calls must be performed by threads currently holding the lock in a synchronized method or block.

  • We still have a while loop: wait calls must occur in a loop. Otherwise, our waiting thread might be asleep when it’s notified by another thread to wake.

  • Despite the while loop, our call to wait signals the thread scheduler to use processor resources elsewhere

    No more busy wait!

Queue: Guarded block on dequeue

public class Queue {
    private static final int CAPACITY = 5;
    private List<Integer> dataQueue = new ArrayList<>();
    private static Queue uniqueInstance;

    // constructor, getInstance(), enqueue()...

    public synchronized Integer dequeue() {
        while (dataQueue.isEmpty()) {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        }
        notifyAll();
        return dataQueue.removeLast();
    }
}

Producer/Consumer: Conclusion

  • Using synchronization idioms, the execution of two different threads (in this case, Producer and Consumer) can be coordinated.
  • Is synchronization always necessary?
    • no, and in some cases it can be a detriment to performance
    • redundant when applied to threads that possess mutually exclusive, private state