Java Thread Synchronization and Concurrency Part 1
Join the DZone community and get the full member experience.Join For Free
Java thread synchronization and concurrency are the most discussed topics during various design phases of a complex application. There are many aspects of threads, synchronization techniques to achieve great concurrency in an application. The evolution of CPUs over the years (multi-core processors, registers, cache memory, and the main memory (RAM) has lead to some areas that usually developers tend to overlook — like thread context, context-switching, variable visibility, JVM memory model vs CPU memory model.
In this series, we will discuss various aspects of the Java memory model, including how it impacts thread contexts, synchronization techniques in Java to achieve concurrency, race conditions, etc. In this article, we'll focus on the concepts of threads, synchronization techniques, and the memory models of both Java and our CPU.
Let's have a quick recap on some thread-related terminologies and concepts before we delve deep into this topic of threads and synchronization further.
- Lock — a lock is a thread synchronization mechanism.
- Every object in Java has an intrinsic lock associated with it. Threads use the Object's monitor to lock or unlock. A lock can be considered as data that is logically a part of the Object's header in memory. See ReentrantLock for extended capabilities that the monitor cannot achieve.
- Every Object in Java has synchronization methods,
notifyAll()]. Any thread calling these methods obtains a lock on that Object using its monitor. This has to be called using synchronized keyword else and IllegealMonitorStateException will be thrown.
- A signal is a way to notify a thread that it should continue its execution. This is achieved using the Object methods
notifyAll(). Calling the methods,
notifyAll(), singals the thread(s) to wake up what is in the background (by a call to the method,
- Missed signal - The methods
notifyAll()do not save the method calls, nor are they aware if
wait()has been called or not by other threads. If a thread calls
notify()before the thread to be signaled has called
wait(), the signal will be missed by the waiting thread. This may cause a thread to wait endlessly because it missed a signal.
Runnableis a functional interface that can be implemented by any class in an application so that a thread can execute it.
volatileis another keyword assigned to variables to make classes thread-safe. To understand the usage of this keyword, the CPU architecture and JVM memory model has to be understood. We'll cover this later.
ThreadLocalenables the creation of variables that can only be read/written by the owner thread. This is used to make code thread-safe.
- Thread Pool is a collection of threads, where the threads will be executing tasks. The creation and maintenance of threads are very controlled by a service. In Java, a thread pool is represented by an instance of the ExecutorService.
ThreadGroupis a that class provides a mechanism for collecting multiple threads into a single object and allows us to manipulate/control those threads all at once.
- Daemon thread — These threads run in the background. A good example of daemon thread is the Java Garbage Collector. The JVM does not wait for a daemon thread before exiting to complete its execution (while JVM waits for non-daemon threads or user threads to finish it's execution).
- synchronized — keyword to control the code execution by a single thread when various threads have to execute the same piece of functionality in a concurrent mode. This keyword can be applied for methods and for code blocks to achieve thread-safety. Note that there is no timeout for this keyword, so there is potential for dead-lock situations.
- Dead-lock - a situation wherein one or more threads are waiting for an object lock to be released by another thread. A possible scenario that causes dead-locks could be where threads are waiting for each other to release the lock!
- Spurious wake-ups - For inexplicable reasons, it is possible for threads to wake up even if
notifyAll()have not been called. This is a spurious wake-up. To cover this issue, the thread awakened spins around a condition in the spin lock.
Thread starvation occurs when a thread is not granted CPU time because other threads are hogging all of it. (E.g. threads waiting on an object (that has called
wait()) remain waiting indefinitely because other threads are constantly awakened instead (by calling
In order to mitigate such conditions, we can set a priority for a thread, using the
Thread.setPriority(int priority) method. The priority parameter has to be within a set range between
Thread.MAX_PRIORITY. Check the official Thread documentation for more information on thread priority.
You may also like: Java Thread Tutorial: Creating Threads and Multithreading in Java
Lock Interface vs Synchronized Keyword
- Having a timeout in a synchronized block or method is not possible. This could end up in scenarios where the application appears to be hung, in a dead-lock, etc. A synchronized block must be contained within a single method only.
- An instance of the Lock interface can have its calls to
unlock()in separate methods. Additionally, Locks can also have timeouts as well. These are two great benefits when compared to the synchronized keyword.
The following is a simple implementation of a custom lock class using native
notify() methods. Please read the comments in the code block below, which gives more information on the
There are two ways by which we can execute a thread in Java. They are:
- Extending the Thread class and calling the
start()method. (This is not a preferred way of sub-classing a class from Thread, as it reduces the scope of adding more functionality of the class.)
- Implementing the
Callableinterface. Both the interfaces are functional interfaces, which means that both have exactly one abstract method defined. (A preferred approach as a class can be extended in the future by implementing other interfaces as well.)
This is a fundamental interface used to execute a particular task by a thread. This interface describes only one method, called
run() with a
void return type. Implement this interface if any functionality has to be executed in a thread but there is no return type expected. Basically, the result of the thread or any exception or error cannot be retrieved in cases of failure.
This is an interface used to execute a particular task by a thread in addition to get a result of execution. This interface follows generics. It describes only one method, called
call(), with a return type described, per the class that implements this interface. Implement this interface if any functionality has to be executed in a thread and the result of the execution has to be captured.
As described above, a thread can be synchronized using the
synchronized keyword or by using an instance of a Lock. A fundamental implementation of the Lock interface is the
ReentrantLock class. Also, there are variations of the Lock interfaces for read/write operations.
This helps the application to achieve higher concurrency when threads are attempting to read or write to a resource. This implementation is called
ReentrantReadWriteLock. The major differences between the two classes are shown below:
|Class ReentrantLock||Class ReentrantReadWriteLock|
|Give access to only 1 thread for either reading or writing but not both.||Gives access to multiple/all threads at a time if the opeartion is reading a resource.
Only one thread at a time will be given access if the operation is a write.
|Locks a resource for both read and write operations making the operations mutually exclusive.||Has separate locks for read and write operations.|
|Reduces performance as the resource is locked even for read operations.||Better in terms of performance as it gives concurrent acces to all threads who want to perform read operations.|
ReentrantReadWriteLock example below to see how to achieve concurrent reads on a resource while allowing only one thread to update a resource.
Note: a resource can be any data that various threads in an application try to access concurrently.
Create one instance of the above class and pass it to multiple threads; the following will be handled:
- Either the
readLockis used by 'N' threads or
writeLockis used by at-most one thread.
- Never both a read or write happens at the same time.
Java Memory Model and CPU
A note on the Java and CPU memory models will help us better understand how objects and variables are stored in the Java Heap/Thread-stack vs the actual CPU memory. A modern-day CPU consists of Registers, which act as the direct memory of the processor itself, Cache Memory — every processor has a cache layer to store data, and finally the RAM or main memory, where application data is present.
On the hardware or CPU, both the Thread Stack and Heap are located in main memory. Parts of the Thread Stack and Heap may sometimes be present in the CPU Cache and in internal Registers. The following are issues that can occur due to the above architecture:
- Visibility of thread updates (writes) to shared variables are not immediately seen by all threads accessing the variables.
- Race conditions when reading, checking, and updating data of the shared variables.
The volatile keyword was introduced in Java 5 and has significant use in achieving thread safety. This keyword can be used for primitives as well as objects. The usage of the
volatile keyword on a variable ensures that the given variable is read directly from main memory and written back to the main memory when updated.
There is a very good article on volatile keyword in DZone. Please refer this link to gain better understanding on this concept and best use of it.
A final topic in thread synchronization is after Lock is the Java class,
ThreadLocal. This class enables the creation of variables that can only be read/written by the same thread. This gives us a simple way to achieve thread safety by defining a thread local variable.
ThreadLocal has significant usage in thread pools or the
ExecutorService, so that each thread uses its own instance of some resource or object.
For example, for every thread, a separate database connection is required, or a separate counter is required. In such cases,
ThreadLocal helps. This is also used in Spring Boot applications, where the user context is set for every incoming call (Spring Security), and the user context will be shared across the flow of the thread through various instances. Use
ThreadLocal for the following cases:
- Thread confinement.
- Per thread data for performance.
- Per thread context.
That's it on thread synchronization and associated concepts. Concurrency will be covered in the part 2 of this article.
Opinions expressed by DZone contributors are their own.