Concurrency
Goals
- Encounter higher-level locking and concurrency constructs.
- Learn to work with read/write locks.
- Understand atomic variables.
- Explore concurrent collections.
Concepts
- compare-and-swap (CAS)
- concurrency
- consistency
Library
java.lang.Double.doubleToRawLongBits(double)
java.lang.Double.longBitsToDouble(long)
java.util.Collections.newSetFromMap(Map<E,Boolean> map)
java.util.concurrent
java.util.concurrent.ConcurrentHashMap<K,V>
java.util.concurrent.CopyOnWriteArrayList<E>
java.util.concurrent.CopyOnWriteArraySet<E>
java.util.concurrent.atomic
java.util.concurrent.atomic.AtomicInteger
java.util.concurrent.atomic.AtomicLong
java.util.concurrent.atomic.AtomicLong.addAndGet(long delta)
java.util.concurrent.atomic.AtomicLong.get()
java.util.concurrent.atomic.AtomicLong.set(long newValue)
java.util.concurrent.locks
java.util.concurrent.locks.Lock
java.util.concurrent.locks.Lock.lock()
java.util.concurrent.locks.Lock.unlock()
java.util.concurrent.locks.ReadWriteLock
java.util.concurrent.locks.ReadWriteLock.readLock()
java.util.concurrent.locks.ReadWriteLock.writeLock()
java.util.concurrent.locks.ReentrantLock
java.util.concurrent.locks.ReentrantReadWriteLock
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.
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.
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.
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:
- An iterator will only see a
snapshot
of the data at the time when the iterator was created. - Extra memory will be used when writes are performed the entire array will be duplicated temporarily.
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>
:
null
keys are not allowed.- An iterator will only see a
snapshot
of the data at the time when the iterator was created. - Extra memory will be used because of the multiple copies of data maintained.
Review
Gotchas
- To use
Lock
retrieved from aReadWriteLock
, you must call itslock()
orunlock()
method. Merely callingReadWriteLock.readLock()
orReadWriteLock.writeLock()
only retrieves the lock, but doesn't do anything with it. - Don't forget to unlock a read/write lock once you are finished using it. Use
try … finally
to make sure your locks are relinquished. Make sure paired lock()/unlock() method calls refer to the same read/write lock. - Concurrent collections don't necessarily prevent race conditions. If a thread queries a concurrent collection and then performs some operation on the collection based on the result, the collection may have changed in the meantime.
In the Real World
- You've learned that programming to interfaces allows any implementation adhering to the interface contract to be substituted for any other. That works fine in a single-threaded context, but there are with various ways of dealing with concurrency the methods returning the implementations must be clear about how the collections should be used. For example, must you synchronize on a list before iterating over its contents (necessary with an
ArrayList<E>
but not needed with aCopyOnWriteArrayList<E>
)? Only the method returning the collection will know which approach to take and therefore its API must be clear about what sort of currency approach is expected.
Think About It
- Do multiple threads need to read the data but never need to write to it after the initial setup? You don't need any concurrency mechanism at all.
- If threads read the data more often than write to it, a
ReadWriteLock
or aCopyOnWriteArrayList
can improve throughput. - If threads read and write data concurrently but it doesn't matter if whether the threads see the most up-to-date information from the other threads, a concurrent collection such as
ConcurrentHashMap<K,V>
may be appropriate, if the extra memory usage can be tolerated.
Self Evaluation
- How could read/write locks make a program run faster than using
synchronized
? - How could one use locks to duplicate the functionality of
synchronized
? What benefit(s) would this bring? - What benefit do atomic variables bring over simply making a variable
volatile
? - Under what conditions would it not be efficient to use a
CopyOnWriteArrayList<E>
? - Must you synchronize on the list when iterating over the elements of a
CopyOnWriteArrayList<E>
? - Must you synchronize on the map when iterating over the entries of a
ConcurrentHashMap<K,V>
?
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
- High Level Concurrency Objects (Oracle - The Java™ Tutorials)
- Java 8 Concurrency Tutorial Part 3: Atomic Variables and ConcurrentMap (Benjamin Winterberg)
Acknowledgments
- Some symbols are from Font Awesome by Dave Gandy.