Skip to content

Java Concurrency in Practice

August 1, 2013

Java Concurrency in Practice written by Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea. The book is divided into 4 major parts: Fundamentals, Structuring Concurrent applications, liveliness/performance/testing, and advanced topics.

Fundamentals

This chapter covers the definition of thread safety: which is simply code that behaves correctly when accessed by multiple threads. This section covers basic Java concurrency tips such as: immutability, intrinsic locking (synchronized), final fields, volatile fields, safe publication, and atomicity for compound actions.

Other basics such as manipulation of long/double are not atomic without the use of volatile. Pointing out that synchronization is not just about mutual exclusion, but also visibility of changes. Safe construction of object involves not letting the this variable to ‘escape’ during construction: like if you add the newly constructed object to some collection during the constructor. Safely publishing data: final fields, volatile field or AtomicReference, object initialization inside of static initializers, or a strong reference guarded by a lock.

java.util.concurrent contains various concurrent data structures with notables of: ConcurrentHashMap, CopyOnWriteArrayList (better than synchronizedList), Deques (“decks”), Latches, FutureTask, Executors, Semaphores and Barriers.

Structuring Concurrent Applications

The new measurement unit in Java 5 and on is ‘task’, while Thread has become an implementation detail for running tasks. The creation of the Executor framework provides a safer, more efficient way to manage concurrent tasks. The executor framework allows flexible concurrency policies such as task scheduling, thread pooling, and handling of results.

Threads/tasks can not forcibly terminated. Even ‘interrupting’ a thread doesn’t stop a thread, it merely requests that the thread save it’s state and stop, but there is no requirement that the Thread must actually do that. The ExecutorService API can be used to both terminate the tasks gracefully and abruptly. UncaughtExceptionHandler is called if a thread terminates abnormally due to an uncaught exception.

Whenever you submit non-independent tasks to an Executor there is always a possibility of thread starvation deadlock. For instance, a single thread executor that spawns another task to the same Executor and waits for that results will deadlock. The correct number of threads on a particular machine is important, as too many will cause too much context switching, and too few won’t fully utilize the processors.

N(threads) = N(cpu) * U(cpu) * (1 + W/C)
Ncpu = number of CPUs = Runtime.getRuntime().availableProcessors()
Ucpu = target CPU utilization 0 <= x <= 1
W/C = ratio of wait to compute time.

Multithreaded event GUI applications are too difficult to be useful, which is why Java specifically made the decision to now implement it. Graham Hamilton from Sun provided a great explanation on why this was not worth the effort. This is why most GUI system are built as single threaded event threads that paint the UI.

Liveness, Performance, and Testing

Intrinsic locks (synchronized) can easily cause deadlock if lock acquisition is not consistently ordered (or if there is a cycle in the locking graph) because these lock acquisition requests do not timeout. A Lock is far more feature rich and effective way to avoid deadlocks, because you can ‘tryLock’ instead of waiting indefinitely for a lock which may never become free. The disadvantage of using a Lock is that you must manage the lock, and remember to release it in a finally clause (or something similar). Also when analyzing thread dumps to debug deadlocks, intrinsic locks are a bit easier to diagnose.

Amdahl’s Law describes the speed up one can expect from concurrent programming. Concurrent programming should only be added in terms of specific performance requirements and no more. Measure don’t guessThe goal of concurrency is to operate in parallel to take advantage of multiple processors, however shared data concurrency requires serialization (sequential execution) at various synchronization points. Concurrent programming is also not cheap: thread context switching, memory synchronization between threads, and lock contention. Lock contention is a major inhibitor of performance, and the goal should be to reduce lock contention as much as possible and for as short as possible.

A performance benchmark of interest was that ConcurrentHashMap was faster throughput that ConcurrentSkipListMap and far faster than both synchronizedMap HashMap and synchronizedMap TreeMap, because synchronizedMaps lock the entire map for operations.

Testing concurrent application for correctness can be extremely challenging because some failure conditions have factors that can not be easily reproduced in test. Test concurrency in Java is more difficult because of garbage collection delays, JIT compilation and code optimization. The best techniques to test concurrent code also include static analysis tools like FindBugs and old fashioned code reviews by other developers.

Advanced Topics

By default all locks in Java are not fair. Fair locks can by used by passing true in the lock constructor, however fair locks have a major performance penalty. ReadWriteLock can allow multiple readers to access a guarded object to increase concurrency. Despite all the advantages of explicit locks in Java, the recommendation is to use intrinsic locks (synchronized) unless there are features you need from explicit locks that are worth the extra effort.

State-dependent locks should make use of existing libraries if possible such as: Semaphore, BlockingQueue, or CountDownLatch. If that is insufficient, then using AbstractQueueSynchronizer and explicit Conditions maybe be necessary.

Atomic variables should be used as opposed to volatile in all but the most trivial cases. java.util.concurrent.atomic contains various lock-free but also thread-safe ways to interact with data atomically. Being lock-free is a major benefit as locking is a form of serialization and is expensive. Performance: under high contention atomic references do not perform better than locks, however until moderate contention they do. So under typical conditions non-blocking algorithm perform better, however they are hard to implement.

Reading about the Java Memory Model and the “happens before” definition in the JLS7 is important to understand that the JIT compiler is free to reorder operations as it sees fit unless their are strict happens before rules setup by proper synchronization.

 

Advertisements

From → Books, Java

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: