Best Practices for Multithreading in Java: Synchronization and Communication, Lecture notes of Design

Best practices for multithreading in java, focusing on synchronization and reliable communication between threads. Topics include the importance of synchronizing access to shared mutable data, using volatile variables, and avoiding overuse of synchronization. It also covers alternative approaches to synchronization and the use of concurrency utilities.

Typology: Lecture notes

2021/2022

Uploaded on 09/12/2022

thecoral
thecoral 🇺🇸

4.5

(30)

395 documents

1 / 3

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
BEST PRACTICES FOR MULTITHREADING IN
JAVA
This article lays down some of the best practices which you can use during your design or code reviews,
and assumes that you are familiar with the basics of mutithreading in java available
at articles/multithreading. I have learned many of these best practices from Effective Java by Joshua
Bloch, which tells how to use java to best effect.
Best Practices for Multithreading in Java
1. If you have shared mutable data, synchronize access to it. Synchronization makes sure that an
object is seen only in a consistent state by any thread, as it ensures mutual exclusion. It also
guarantees reliable communication between threads as it ensures that each thread entering a
synchronized region sees the effect of all previous modifications that were guarded by the same lock.
2. For reliable communication between threads, value written by one thread on a variable should be
visible to the other immediately. Even though reading or writing a variable other than log or double is
atomic, java doesn’t give a guarantee that a value written by one thread on a variable will be
visible to the other immediately, unless both read and write to it is guarded by synchronization or
the variable is a volatile variable.
3. Volatile variable guarantees that a value written by one thread will be visible to the other immediately,
but does not provide any mutual exclusion. Therefore, when you need only reliable communication by
making variables visible to other threads, but no mutual exclusion, use volatile, as it has slight
performance advantage over synchronization. However for compound statements like ‘myVar++’, you
should use synchronization or consider using atomic variables in java.util.concurrent.atomic package
like AtimicLong, which has a getAndIncrement() method, which is atomic.
4. Alternate approaches to synchronization are:
a. Share only immutable data.
b. Do not share at all.
c. Confine mutable data to a single thread.
d. Safely publish effectively mutable data. These are objects where one thread modifies the data for
sometime and then share it with other threads, so that synchronization is only required for the act of
sharing.
5. Inside a synchronized region, never call a method designed for inheritance or one provided by
client like a method of an object passed in, as these will allow client to take control and may result in
deadlock. We should use alien methods outside synchronized region.
6. Over use of synchronization should be avoided as it might result in deadlock, reduced performance
due to improper parallelization and even reduce JVM’s code optimization scope. You should do only very
less work as required inside synchronized regions. If a code can be moved outside a synchronized
region, it should be moved out.
pf3

Partial preview of the text

Download Best Practices for Multithreading in Java: Synchronization and Communication and more Lecture notes Design in PDF only on Docsity!

BEST PRACTICES FOR MULTITHREADING IN

JAVA

This article lays down some of the best practices which you can use during your design or code reviews, and assumes that you are familiar with the basics of mutithreading in java available at articles/multithreading. I have learned many of these best practices from Effective Java by Joshua Bloch, which tells how to use java to best effect.

Best Practices for Multithreading in Java

  1. If you have shared mutable data, synchronize access to it. Synchronization makes sure that an object is seen only in a consistent state by any thread, as it ensures mutual exclusion. It also guarantees reliable communication between threads as it ensures that each thread entering a synchronized region sees the effect of all previous modifications that were guarded by the same lock.
  2. For reliable communication between threads, value written by one thread on a variable should be visible to the other immediately. Even though reading or writing a variable other than log or double is atomic, java doesn’t give a guarantee that a value written by one thread on a variable will be visible to the other immediately, unless both read and write to it is guarded by synchronization or the variable is a volatile variable.
  3. Volatile variable guarantees that a value written by one thread will be visible to the other immediately, but does not provide any mutual exclusion. Therefore, when you need only reliable communication by making variables visible to other threads, but no mutual exclusion, use volatile , as it has slight performance advantage over synchronization. However for compound statements like ‘myVar++’, you should use synchronization or consider using atomic variables in java.util.concurrent.atomic package like AtimicLong, which has a getAndIncrement() method, which is atomic.
  4. Alternate approaches to synchronization are: a. Share only immutable data. b. Do not share at all. c. Confine mutable data to a single thread. d. Safely publish effectively mutable data. These are objects where one thread modifies the data for sometime and then share it with other threads, so that synchronization is only required for the act of sharing.
  5. Inside a synchronized region, never call a method designed for inheritance or one provided by client like a method of an object passed in, as these will allow client to take control and may result in deadlock. We should use alien methods outside synchronized region.
  6. Over use of synchronization should be avoided as it might result in deadlock, reduced performance due to improper parallelization and even reduce JVM’s code optimization scope. You should do only very less work as required inside synchronized regions. If a code can be moved outside a synchronized region, it should be moved out.
  1. Should be careful while calling other methods holding the same lock and making use of Java’s reentrant locking, as there is a chance to call an unrelated operation in progress guarded by the same lock. If we call alien methods they might also do so and we won’t have any control.
  2. You must synchronize access to mutable static fields as there can be no guarantee that unrelated clients might do it externally. However for other variables and methods, in general, or when in doubt whether to synchronize, we can decide not to synchronize, but should document that it is not thread safe.
  3. You should prefer using internal locks to entire object locking , by not synchronizing on the ‘this’ object lock. You can create some private lock objects (private Object lock = new Object();) and use them for related operations. When you synchronize internally you can make use of many techniques cuch as lock splitting, lock stripping, and non-blocking concurrency control to achieve high concurrency.
  4. When writing new code or refactoring code, use concurrency utilities such as executor framework, concurrent collections, and synchronizers in preference to wait and notify.
  5. When writing new code or refactoring code, do not use threads directly , but use the executer framework that consists of executors and tasks, that splits the duties of a thread. A task is the actual unit of work, and the executor service executes them. A task can be a Runnable or a Callable. Callable is like Runnable except that it returns value. Executor service can do many things such as wait for tasks to complete, wait for graceful termination, retrieve the results of tasks etc.
  6. Use the new concurrent collections such as a ConcurrentHashMap in preference to synchronized collections like Collections.synchronizedMap or Hashtable. ConcurrentHashMap extends Map and include methods cuch as putIfAbsent. Refer to java.util.concurrent package javadoc to see if you already have a concurrent collection for your requirement.
  7. Decide the best thread pool implementation while working with executors based on requirement. For example, Executors.newCashedThreadPool can be used for short lived asynchronous tasks on a lightly loaded server, whereas for long running threads on heavily loaded production servers use Executors.newFixedThreadPool. Refer to java doc when in doubt.
  8. Use Synchronizers for coordinating thread activities like waiting for another thread. Examples for synchronizers are CountDownLatch, Semaphore, CyclicBarrier and Exchanger.
  9. The wait method , if still used in code, must be invoked inside a synchronized region that locks on the object on which it is invoked and also, we should use the wait-loop idiom always, where you invoke wait inside a loop. The loop will test the condition before and after waiting. Testing the condition before is required to ensure liveness because if the condition already holds and notify or notifyAll has been already invoked, there is no guarantee that thread will ever wake up from wait. Testing the condition after is necessary because if the thread proceeds with the action when the condition does not hold, the results can be unexpected. A thread might wake up when the condition does not hold due to many reasons such as another thread could have changed the condition between the notify invocation and wakeup, another thread could have invoked notify accidentally, a notifyAll was invoked even if condition is not satisfied for some threads, or a thread could rarely wakeup in the absence of notify, known as a spurious wakeup.