- 66. Synchronize access to shared mutable data
- 67. Avoid excessive synchronization
- 68. Prefer executors and tasks to threads
- 69. Prefer concurrency utilities to wait and notify
- 70. Document thread safety
- 71. Use lazy initialization judiciously
- 72. Don't depend on the thread scheduler
- 73. Avoid thread groups
synchronizedkeyword makes sure that at the same time, there is only thread accessing all the synchronized methods in the object.
66. Synchronize access to shared mutable data
The way to stop a thread interrupting another thread, the thread should poll a boolean field, which initialized with
false, but the second thread can set it to
synchronize, it is not guaranteed that backgoundThread sees the changed
stopRequested. The VM will compile the
while loop into:
This is an optimization called Hoisting.
When a new thread start, it will first read and load
stopRequested from the main memory, which means the threads keeps a copy of
stopRequested in the thread working memory. Until the thread finishes, it will only use the copy.
Solution to this is:
We synchronized both reading and writing. Only synchronize writing will not actually work.
Another solution is to use
volatile keyword. It means that the variable in the thread will not use the copy. It will be notified when the variable is updated.
But the purpose of
volatile is to use the latest value. It does not guarantee synchronization.
Example: generating sequential number
++ operation on
nextNumber is not atomic. This program may fail.
One solution is to add
Another way is to use Java API
java.util.concurrent.atomic.AtomicLong. It is what we want, and it may perform better.
Limit variable data in a single thread. Some one calls your program may put it in a multiple threading environment.
67. Avoid excessive synchronization
This example uses composite-over-inheritance design to create an observer class.
Currently this program works fine. It prints out the numbers from 0 to 99.
If we change the observer in the
We expect the program to end after printing 23. This will be illegal, since we are trying to delete an element from the set DURING we are iterating the set.
notifyElementAdded is a synchronized block.
Continue the example
Now we are trying to unsubscribe the observer using another thread.
Executor service is provided by
The new observer is added before the previous one. It encounters deadlock. Because in the main thread,
notifyElementAdded(), the later locks
observers. And the newly added observer also want to gain the lock.
Calling external method, a method from outside of the containing class, always causes deadlock. We can move the external method out of the synchronized block, by making a snapshot, and operate on the snapshot.
A better way to move external method out of synchronized code block is by using concurrent collection (since Java 1.5). This is a variant of
ArrayList, does all the operations in a copy of the low level array. Therefore it does not require synchronization, and the performance is good. If the program changes the array a lot, the performance will not be good. But for the observer list here, it is very good.
68. Prefer executors and tasks to threads
Executor is an interface-based task executor. To use it:
If you want to use more than one thread to deal with a task queue, just use a executor service factory(which actually is a thread pool), or directly
ThreadPoolExecutor class in Java.
For a small project,
Executors.newCachedThreadPool is a good choice, but not for a big project. It may make the server overloaded.
Previously, thread is both the working unit and the working mechanism. Now we need toe separate them. Working unit should be
Runnable returns value. Working mechanism is executor service.
ScheduledThreadPoolExecutor in Executor Framework can replace
Please refer to Java Concurrency in Practice for more details on Executor Framework.
69. Prefer concurrency utilities to wait and notify
From Java 1.5,
java.util.concurrent has three kinds of tools: Executor Framework, Concurrent Collection and Synchronizer.
It provides high performance concurrency for traditional Collections like
Queue. If you use concurrent collection, make sure you have concurrent job inside, otherwise you will only slow down your program.
ConcurrentMap extends from
ConcurrentHashMap is even better.
Most of the
ExecutorServices have implemented BlockingQueue. They will be blocked until operation finishes.
It makes thread waiting for another thread, and collaborate. Most common synchronizers are
CountDown Latch is a single-use barrier, makes threads waiting for other threads.
Example: CountDown Latch
Suppose we have a batch of tasks, they need to start at the same time. Before they start, they need to get ready. Once all the tasks are ready, they start together, and a timer starts counting. Once last task finishes, the timer stops counting.
start.await() means wait here, until the
done.await() waits there, until it becomes 0, then return the running time.
System.nanoTime instead of
70. Document thread safety
A method documented with
@synchronized does not mean that it is completely thread-safe.
There are several thread security levels:
- Immutable: the instance is immutable, no need external synchronization. eg.
- Unconditionally thread-safe: the instance can be changed, but it has sufficient internal synchronization that its instances can be used concurrently without the need for any external synchronization. eg.
- Conditionally thread-safe: some methods require external synchronization for safe concurrent use.
- Not thread-safe: instances are mutable. Client must surround each method invocation with external synchronization if the method will be used concurrently.
- thread-hostile: This class is not safe even if all method invocations are surrounded by external synchronization.
Thread-safe type should be documented.
Private lock object
Private lock object can be an alternative of synchronized block.
Private lock object can only be applied to unconditional thread-safe. Because in conditional thread-safe method, you must document which lock the user has to gain.
71. Use lazy initialization judiciously
Lazy initialization is an optimization technique.
Lazy initialization should only be used when the initialization cost is high.
If multiple threads are using a lazy-initialed object, you should be careful about the initialization.
A typical way to initialed an object:
If we use lazy initialization, that part needs to be synchronized:
If we need to lazy initiate a static field, we can use lazy initialization holder class.
getField() get called for the first time, it accesses
FieldHolder.field, which makes
FieldHolder class initialized. Later
getField() will return the created object. The point is we don't need to add
getField(), and the code is safe.
72. Don't depend on the thread scheduler
Any program that replies on the thread scheduler for correctness or performance is likely to be nonportable.
We'd better keep the average number of threads running to be less than the number of CPU cores. To achieve that, we need to make thread do more meaningful tasks. Meanwhile keep thread pool small.
Don't make thread in a busy-wait state, repeating checking the state of a shared object.
Thread.yield to gain more CPU time for a thread. This is nonportable. You should redesign the application and decrease the number of concurrent threads.
Thread.yield should only be used in testing.
73. Avoid thread groups
The thread group that Java provided is very insecure.
Try to use thread pool executor instead.