Java Thread Synchronization and Concurrency Part 1
Join the DZone community and get the full member experience.
Join For FreeIntroduction
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.
Recapitulation
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,
wait()
andnotify()
[alsonotifyAll()
]. 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
wait()
,notify()
, andnotifyAll()
. Calling the methods,notify()
ornotifyAll()
, singals the thread(s) to wake up what is in the background (by a call to the method,wait()
). - Missed signal - The methods
notify()
andnotifyAll()
do not save the method calls, nor are they aware ifwait()
has been called or not by other threads. If a thread callsnotify()
before the thread to be signaled has calledwait()
, the signal will be missed by the waiting thread. This may cause a thread to wait endlessly because it missed a signal. Runnable
is a functional interface that can be implemented by any class in an application so that a thread can execute it.volatile
is 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.ThreadLocal
enables 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.
ThreadGroup
is 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
notify()
andnotifyAll()
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.
xxxxxxxxxx
public synchronized doWait() {
while(!wasSignalled) { // spin-lock check to avoid spurious wake up calls
wait();
}
// do something
}
public synchronized doNotify() {
wasSignalled = true;
notify();
}
Thread Starvation
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 notify()
).
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.MIN_PRIORITY
to 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
lock()
andunlock()
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 wait()
and notify()
methods. Please read the comments in the code block below, which gives more information on the wait()
and notify()
methods.
xxxxxxxxxx
class CustomLock {
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException {
isLocked = true;
while(isLocked) {
// calling thread releases the lock it holds on the monitor
// object. Multiple threads can call wait() as the monitor is released.
wait();
}
}
public synchronized void unlock() {
isLocked = false;
notify();
// only after the lock is released in this block, the wait() block
// above can re-acquire the lock on this object's monitor.
}
}
Thread Execution
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
Runnable
orCallable
interface. 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.)
Runnable Interface
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.
Callable Interface
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.
Synchronization Techniques
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. |
See the 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.
xxxxxxxxxx
public class ConcurrentReadWriteResourceExample {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private void readResource() {
readLock.lock();
// read the resource from a file, cache, database or from memory
// this block can be accessed by 'N' threads concurrently for reading
readLock.unlock();
}
private void writeResource(String value) {
writeLock.lock();
// write or update value to either a file, cache, database or from memory
// this block can be accessed by at-most '1' thread at a time for writing
writeLock.unlock();
}
}
Create one instance of the above class and pass it to multiple threads; the following will be handled:
- Either the
readLock
is used by 'N' threads orwriteLock
is 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.
Volatile Keyword
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.
ThreadLocal Class
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.
xxxxxxxxxx
/**
* This is a demo class only. The ThreadLocal snippet can be applied
* to any number of threads and you can see that each thread gets it's
* own instance of the ThreadLocal. This achieves thread safety.
*/
public class ThreadLocalDemo {
public static void main(String...args) {
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
protected String initialValue() {
return "Hello World!";
}
};
// below line prints "Hello World!"
System.out.println(threadLocal.get());
// below line sets new data into ThreadLocal instance
threadLocal.set("Good bye!!!");
// below line prints "Good bye!!!"
System.out.println(threadLocal.get());
// below line removes the previously set message
threadLocal.remove();
// below line prints "Hello World!" as the initial value will be
// applied again
System.out.println(threadLocal.get());
}
}
That's it on thread synchronization and associated concepts. Concurrency will be covered in the part 2 of this article.
Further Reading
Opinions expressed by DZone contributors are their own.
Comments