Effective Java - Concurrency
Concurrency (Ch 10)
This is a short summary of Joshua Blochs book Effective Java chapter 10. I have only included items that I find relevant.
Synchronize access to shared mutable data (Item 66)
Reading or writing a variable other than long
or double
is atomic, so why use synchronized
? Synchronized is required for reliable communication between threads as well as for mutual exclusion.
Here, one thread is stopping the other using a boolean flag. This program will run forever, because jvm can optimize (hoisting)
to
Fix:
So, in this case, the use of synchronized
is to solely to communicate between threads, and not mutual exclusion, because the actions in the synchronized
methods would be atomic even without the keyword.
In this case however, a better solution is to declare stopRequested
as volatile
which is less verbose and performs better. volatile
performs no mutual exclusion but guarantees that any thread that reads the field will see the most recently written value.
volatile
won’t work in the following case:
Here, the nextSerialNumber
is first read and then incremented. Consider if a thread is generating a new serial number. If a second thread reads the value at the moment the first thread is reading before incrementing, the second thread gets an old value. There are two solutions: remove volatile
and use synchronize
or remove volatile
and use AtomicLong
. The best solution however, is to avoid sharing mutable data!
Avoid excessive synchronization (item 67)
Synchronization can cause reduced performance, deadlock or even nondeterministic behaviour.
- Inside a synchronized region, do not invoke a method that is designed to be overridden, or one provided by a client in the form of a function object. If that method indirectly (e.g. by calling another method in another thread) requires a lock to an object already locked by the synchronized block, a deadlock occurs.
- Solution to the above problem is to move the alien invocation outside of synchronized block. A better solution can be to use
CopyOnWriteArrayList
where all write operation perform a copy of the underlying array. This method can avoid synchronized blocks entirely (see item for example). - Do as little as possible inside synchronized regions.
- Performance cost of synchronization: Lost opportunities of CPU parallelism, and limiting JVM optimizations.
- When in doubt do not synchronize, but document thread-unsafety and let the client do synchronization. However, if you know that your objects will be used in multithreaded environments, provide threadsafety (or threadsafe versions) because internal locking is faster than externally locking the entire object.
Prefer executors and tasks to threads (item 68)
Executors and tasks can be used instead of threads to execute parallel execution.
- Use
Executors.newCachedThreadPool
for small programs or light loaded servers because it demands no configurations and has a good default. It is not good for production because it does not queue threads and floods the CPU. - Use
Executors.newFixedThreadPool
for production. - Use
ScheduledThreadPoolExecutor
for timed executions.
Prefer concurrency utilities to wait and notify (item 69)
Higher level concurrency utilities: The executor framework (item 68), concurrent collections and Synchronizers.
Concurrent Collection:
- Concurrent collections manage their own synchronization internally.
- Some concurrent collections are extended with state-dependent modify operations like
ConcurrentMap.putIfAbsent
- use
ConcurrentHashMap
in preference toCollections.synchronizedMap
orHashTable
.
Synchronizers are objects that enable threads to wait for one another for coordination. Example: CountDownLatch and Semaphore.
- For interval timing, always use
System.nanoTime
in preference toSystem.currentTimeMillis
because it is more accurate, precise and not affected by adjustments to the system’s real-time clock.
Always use the utilities mentioned above instead of wait and notify as there is seldom any reason to use them in new code.
Use lazy initialization judiciously (item 71)
If you must initialize a field lazily in order to achieve performance goals, or to break a harmful initialization circularity, then use appropriate lazy initialization techniques. For instance fields, it is the double-check idiom; for static fields, the lazy initialization holder class idiom. For instance fields that can tolerate repeated initialization, you may also consider the single-check idiom.
Do not depend on the thread scheduler (item 72)
A thread scheduler determines which thread gets to execute next. Do not make assumptions and depend on thread scheduler policy when programming because it is likely to be non-portable (OS).