Concurrency

Goals

Concepts

Library

Lesson

Larger programs have more moving parts, and to get things done more quickly it's tempting to have several of those parts move at the same time. As programs grow more connected to other programs across the network, it is more likely some input or output will be processed concurrently. As you've seen in previous lessons, low-level synchronization is a tedious, brute-force approach to concurrency that sometimes seems to introduce more problems than it solves, including the risk for deadlock.

The Java library provides some tools that allow one to approach from higher-level concepts. Some of these tools use synchronization beneath the surface, but hide the details from the developer. Others dispense with synchronization altogether, playing logic tricks to prevent race conditions without the overhead of synchronized blocks. The use of Java concurrency tools can make your multithreaded program run faster and lower the risk of errors. Most but not all of these tools are located in the java.util.concurrent package.

Read/Write Locks

The use of the synchronization keyword can slow a program down. If all methods of a class are marked as synchronized, this imposes a bottleneck on any thread accessing the object. For a fully synchronized class, a multithreaded program is effectively reduced to single-threaded processing, as all other threads wishing to access the data block until the single thread leaves the synchronized section.

But not data access is the same; some threads modify data, but other threads may only wish to read the data. Indeed in many situations read operations are much more common than write operations which potentially result in data modifications. Consider a bank account allowing a caller to retrieve a combined balance of checking and savings.

BankAccount with synchronized checking and savings balances.
public class BankAccount {

  private final Object balanceLock = new Object();

  private long checking;

  private long savings;

  public long depositChecking(final long amount) {
    synchronized(balanceLock) {
      return checking += amount;
    }
  }

  public long depositSavings(final long amount) {
    synchronized(balanceLock) {
      return savings += amount;
    }
  }

  …

  public long getCombinedBalance() {
    synchronized(balanceLock) {
      return checking + savings;
    }
  }

}

Race conditions result when data modification logic is not atomic and may result in inconsistencies with other reading threads. If threads are only reading data, no race condition can occur. In the example above it would pose no problem for multiple threads to read the combined balance concurrently—if only there were some way to guarantee that none of the accessing threads would write to the data while a thread was reading it.

In the java.util.concurrent.locks package Java provides the java.util.concurrent.locks.ReadWriteLock interface for managing access to a resource, allowing which multiple threads to read data concurrently and only blocking threads that would modify the shared information. A read/write lock is actually merely a facade with methods ReadWriteLock.readLock() and ReadWriteLock.writeLock(),each of which returns a java.util.concurrent.locks.Lock for reading or writing, respectively.

A lock provides a Lock.lock() method to acquire the lock, and a Lock.unlock() method to give up the lock, much like how synchronized works. But because a ReadWriteLock provides two locks, threads wanting read access can acquire the read lock, while threads wanting write access can acquire the write lock. Using a ReadWriteLock, multiple threads can acquire the read lock simultaneously, but a thread can only acquire the write lock at the exclusion of all other locks, whether reading or writing.

The read/write lock implementation most often used is the java.util.concurrent.locks.ReentrantReadWriteLock, which allows thread to reenter a section for which it already holds a lock, as synchronized does. It can be used to increase concurrent read access to the bank account shown earlier.

BankAccount with access to checking and savings balances governed by a ReadWriteLock.
public class BankAccount {

  private final ReadWriteLock balanceLock = new ReentrantReadWriteLock();

  private long checking;

  private long savings;

  public long depositChecking(final long amount) {
    balanceLock.writeLock().lock();
    try {
      return checking += amount;
    } finally {
      balanceLock.writeLock().unlock();
    }
  }

  public long depositSavings(final long amount) {
    balanceLock.writeLock().lock();
    try {
      return savings += amount;
    } finally {
      balanceLock.writeLock().unlock();
    }
  }

  …

  public long getCombinedBalance() {
    balanceLock.readLock().lock();
    try {
      return checking + savings;
    } finally {
      balanceLock.readLock().unlock();
    }
  }

}

Atomic Variables

Entering a synchronized area slows down a Java program, and ReadWriteLock implementations still use synchronization behind the scenes. The benefit of ReadWriteLock is not that it removes synchronization checks, but that it selectively allows certain threads to access shared data concurrently—if the code being executed is not supposed to made any modifications.

Java's atomic variable classes in the java.util.concurrent.atomic package dispense with synchronization altogether, potentially speeding up access to data. These classes such as java.util.concurrent.atomic.AtomicLong allow multiple threads to access a value concurrently. Operations such as increment and add are performed atomically, this preventing race conditions. 

You can retrieve an AtomicLong's current value using AtomicLong.get(), and change it by using AtomicLong.set(long newValue). More interesting methods include AtomicLong.addAndGet(long delta), which atomically adds something to the value and returns the result, as shown in the following example updated from the lesson on synchronization.

BankAccount.deposit(…) using AtomicLong.
public class BankAccount {

  private final AtomicLong balance = new AtomicLong();

  /**@return The current balance.*/
  public long getBalance() {
    return balance.get();
  }


  /** Deposits money to the account.
   * @param amount The amount, in cents, to deposit.
   * @return The new balance after the deposit.
   */
  public long deposit(final long amount) {
    return balance.addAndGet(amount);
  }

}

Concurrent Collections

Along with atomic variables, Java provides several collection implementations that are thread-safe. These implementations do not use locks for retrieval, so reading form them can be faster than using synchronized. These classes usually work by maintaining or creating several copies of the data internally, so before using them you should usually consider whether concurrent access using these classes is worth the added memory overhead for your use case.

CopyOnWriteArrayList<E>

As you might guess from the name, java.util.concurrent.CopyOnWriteArrayList<E> is a normal array list that will make a copy of the entire array when a thread tries to modify it. As modification is performed on a copy of the data, it does not prevent other threads from reading from the main list (or writing to other copies). Creating copies of the array slows down the program and uses memory, so this class is only really useful if writing is far less common than reading, and if the other methods of concurrency such as read/write locks are too expensive. Keep in mind:

ConcurrentHashMap<K,V>

A java.util.concurrent.ConcurrentHashMap<K,V> keeps several copies of the map internally, and these are reconciled after updates. Iterating through the map is safe without locking, although the iteration will not see any updates made after iteration has started. There are several caveats with using ConcurrentHashMap<K,V>:

Review

Gotchas

In the Real World

Think About It

Self Evaluation

Task

Increase the possible throughput of FilePublicationRepository implementation by converting it to use ReadWriteLock. Be careful: if your publication query methods have logic to update internal information, they are performing writing which must be performed under a write lock.

Improve the FakePublicationRepository to use a concurrent collection such as CopyOnWriteArrayList<E> or ConcurrentHashMap<K,V>. Make clear in the class documentation that the implementation is thread-safe, but that updates made in one thread may not immediately seen by other threads.

See Also

Acknowledgments