๐ชก
Threadis a unit of a flow where resources are assigned by aprocess.๐งต
Processis a unit of a job where resources are assigned by theOS(Operating System).
A thread or, more accurately, a multi-threading is a critical concept heavily related to creating a highly functioning programme that is specifically capable of handling a considerable volume of user traffic.
In Java, specifically, under the multi-threading context, the invocation to generate a single thread incurs the creation of a JVM stack inside the stack inside the Running Data Areas. A JVM stack is responsible for creating and deleting a frame in which a JVM stack upon the method invocation pushes a frame and the frame records the overall method execution (for more details, please read this).
Data stored in the heap memory and method areas, such as objects and statically declared fields, is accessible to all functioning threads by its design. This means that each thread can concurrently access these shared data, enabling simultaneous operations across multiple threads. This concurrent execution of multiple threads leads directly to increased application performance, as tasks are handled in parallel, improving efficiency and responsiveness. Hence, designing a Java programme as a multi-threading programme may be essential under the high-performance requirements.
However, the inappropriate implementation of multi-threading into a Java programme could cause issues such as:
Synchronisation Problem (Data Corruption)DeadLock where these essentially results in the program errors and poor performance levels.
๐งธ Example
A thread in Java can be implemented either via:
Thread ClassRunnable InterfaceThread Class implements a Runnable Interface and hence, a run() method in the Runnable Interface has to be implemented in order to execute a thread then a start() method within the Thread Class has to run.
The below is a good example of how thread can be implemented in Java via two above methods.
Threads
public class SampleThread extends Thread {
@Override
public void run() {
...
}
}
public class SampleRunnable implements Runnable {
@Override
public void run() {
...
}
}
Results
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.runThread();
}
public runThread() {
Thread thread1 = new SampleThread();
Runnable runnable = new SampleRunnable();
Thread thread2 = new Thread(runnable);
thread1.start(); // starts run() method
thread2.start(); // starts run() method
}
}
Synchronisation Problem refers to multiple threads being able to concurrently access the common resource often stored in heap memory or method area in Java context. This often results in Data Corruption where the anticipated results of the method execution do not appear.
For instance, given the two threads invoking the same method that adds the parameter to the instance int variable count:
Calculator Class
public class Calculator {
private int counter;
public Calculator() {
counter = 0;
}
public void add(int value) {
counter += value;
}
public int getCounter() {
return counter;
}
}
Thread Class
public class SampleThread extends Thread {
private Calculator calculator;
public SampleThread() {
}
public SampleThread(Calculator calculator) {
this.calculator = calculator;
}
@Override
public run() {
for (int i = 0; i < 10000; i++) {
calculator.add(1);
}
}
}
Results
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.runCalculator();
}
public runCalculator() {
Calculator calc = new Calculator();
Thread thread1 = new SampleThread(calc);
Thread thread2 = new SampleThread(calc);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("Total Counter Value: " + calc.getCount());
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
the following result of executing the above Main class, is very well anticipated to be 20000, the actual computational outcomes from this execution, however, vary every single time in which the outcome could be any numbers between 0 ~ 20000. This heavily underlies the fact that a thread by its design, if several become implemented works in parallel than sequential.

Such characteristic strictly results in the two different threads accessing the identical values of counter at the same time, perhaps before the computation of one thread is over, then overriding the counter value to different values repetitively.
Overall, leaving the Synchronization Problem unaddressed can make a program more error-prone such that this have to be addressed under the context of multi-threading programme.
๐ A
deadlockrefers to a situation in computing where none of thethreadsorprocessescan further proceed followed by each waiting for the other to release resources.
A deadlock can occur in a system only when all of the following conditions are met simultaneously.
๐ฉโโค๏ธโ๐จ Mutual Exclusion
- each process tries to use a resource exclusively, preventing other processes from accessing that resource.
๐ฉโ๐ฆฝ Hold and Wait
- a process holds one resource while waiting for another resource.
๐ก No Preemption
- once a resource is allocated to a process, it cannot be forcibly taken away until the process has finished using it.
โญ๏ธ Circular Wait
- each process holds a resource that the next process in the cycle requires, creating a circular dependency.
It is generally considered that addressing any one of the above conditions is sufficient to prevent a deadlock. These conditions can be mitigated through the following approaches:
๐ฉโโค๏ธโ๐จ Mutual Exclusion
- design resources to be shared across all process.
๐ฉโ๐ฆฝ Hold and Wait
- assign all the required resources at once.
๐ก No Preemption
- release the resource and allocate it under the priorities.
โญ๏ธ Circular Wait
- assign the executing orders of the processes.
A deadlock can harm the system's performance and can leave the system to be error-prone in which addressing is necessary.
๐งค
Thread safetyrefers to the property inmulti-threaded programmingwhere afunction,variable, orobjectcan be accessed bymultiple threadssimultaneously without causing the above issues (Synchronisation ProblemsandDeadlock) in the program's execution.More specifically, it is defined as a situation where, if a
functionis being called and executed by onethread, anotherthreadcan call and execute the samefunctionat the same time, and the results of thefunction's execution in each thread will still be correct.
In Java, there are two approaches to achieving thread safety.
Synchronisation & LockThreadLocalwhere these will be discussed in greater detail in the following sections.
๐ A
synchronizationis a mechanism where access to the shared resource becomes restricted to only a singlethreaduntil the flow execution is complete.
In Java, specifically, synchronization can be mostly done in two approaches, either over the methods or the user-specified blocks. These two approaches are termed as the following:
synchronized methodssynchronized blocks These either come with a tentative lock that comes by default or a user-set lock, where the lock becomes the mechanism controlling the access of a thread.
Synchronized Methods can be simply implemented via the synchronized keyword followed by a usual method declaration as below.
Synchronized Method
public synchronized void synchronizedMethod() {
// task execution over the shared resource...
}
The above synchronised method allows only a single thread to access the internal logic and resources within the method.
There also exists a tentative lock over this method approach where the thread entrance is controlled at the instance level. This implies that given the two objects instantiated by the same class, two hypothetical threads accessing these two different instances will not have any influence from the method synchronisation.
Sample Class
public class Sample {
public synchronized void synchronizedMethod() {
// task execution over the shared resource...
}
}
Results
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.runSample();
}
public void runSample() {
Sample sample1 = new Sample();
Sample sample2 = new Sample();
// synchronisation allows one thread to enter.
Thread thread1 = new Thread(() -> {
sample1.synchronizedMethod();
});
Thread thread2 = new Thread(() -> {
sample1.synchronizedMethod();
});
// won't have any impact of methods being synchronised (synchronised at instance level).
Thread thread3 = new Thread(() -> {
sample2.synchronizedMethod();
});
}
}
The above could be a good practical example of method synchronisation at the instance level.
Static Synchronized Methods
public static synchronized void synchronizedStaticMethod() {
// task execution over the shared resource...
}
A method can be also statically declared and accompanied by the same synchronized keyword. This makes a static synchronized method and it being statically declared implies that the method is stored in the method (static) area. This further implies that the logic and resources inside the method can be accessible at the class level, also referring to a tentative lock existing at the class level. Hence, all threads accessing to the static synchronized methods from the objects instantiated by an identical class will not gain access like it was from the above synchrnozed methods.
Sample Class
public class Sample {
public static synchronized void synchronizedMethod() {
// task execution over the shared resource...
}
}
Results
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.runSample();
}
public void runSample() {
Sample sample1 = new Sample();
Sample sample2 = new Sample();
// static synchronisation allows one thread to enter over all instances.
// hence thread2 should wait thread1 or vice versa.
Thread thread1 = new Thread(() -> {
sample1.synchronizedMethod();
});
Thread thread2 = new Thread(() -> {
sample2.synchronizedMethod();
});
}
}
The above could be a good practical example of static method synchronisation at the class level.
A synchronized block consists of the synchronized keyword followed by parentheses () and curly brackets {}. It was designed to enhance efficiency and performance by addressing the inefficiencies of the synchronized method, which can unnecessarily block multiple threads from accessing the entire method. A synchronized block provides developers with greater flexibility, allowing them to synchronise only the critical sections of code that need to be thread-safe.
Another noteworthy aspect of a synchronized block could be that the access level can be customised by specifying it in the parenthesis (). Both instance and class level access can be customised, and a specified object or class are viewed as an explicit lock.
Synchronized Block (Instance Level)
public class SampleBlock {
public void method() {
...
synchronized(this) {
// task execution over the shared resource...
}
...
}
}
Synchronized Block (Instance Level)
public class SampleBlock {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() {
synchronized(lock1) {
// task execution over the shared resource...
}
}
public void method2() {
synchronized(lock2) {
// task execution over the shared resource...
}
}
}
In the example of the SampleBlock class and its method(), a synchronized block can be implemented wherever thread safety is necessary. Using this inside the parentheses () defines the current instance as the lock.
Instance-level synchronization can also be achieved by providing an explicit object as a member variable, an external parameter, or during instantiation via a constructor. This allows developers to control which objects serve as locks for synchronization and hence enhance flexibility. This approach is particularly useful when two methods need to be thread-safe but should not interfere with each other, as it enables separate synchronization for each method.
A good practical example of this could be a banking application, where withdrawal and deposit actions must be individually thread-safe, but should still be able to execute concurrently without blocking each other.
๐ก An
objectspecified in the()is termed as amonitor object.
Synchronized Block (Class Level)
public class SampleBlock {
public void method() {
synchronized(SampleBlock.class) {
// task execution over the shared resource...
}
}
}
Finally, class-level synchronisation can be achieved by referencing a class where a method belongs to namely, CLASS_NAME.class. The synchronized block then functions identically to that of the static synchronized method.
Overall, synchronisation implemented via synchronized keyword in Java, ensures that the synchronised sections become thread-safe. This, however, comes at the cost of lowered performances as any other thread accessing the synchronised section has to wait for the former thread.
ThreadLocalis a mechanism that allows eachthreadto have its own unique data storage.
ThreadLocal is useful for maintaining data consistency and preventing conflicts between threads in a multi-threaded environment, as each thread can store a unique value for the same variable, which is not accessible by other threads. This reduces the need for shared resources and helps ensure thread-safety.
Technically, ThreadLocal uses each thread as a key and maps a user-specified value to that thread. It's important to note that after a thread is finished, the value mapped to that thread must be cleared to prevent newly created threads with the same name or heap address from accessing or manipulating the previous data.
A context in the Spring Security is a good use-case of a ThreadLocal.
Another key noteworthy point is Reentrant Lock, the alternative to the synchronized keyword in Java for achieving synchronisation. This was introduced to introduce features that was not available from synchronisation via methods and blocks.
These are suggestibly:
Reentrancythread can acquire the same lock it holds.Fairnessthread that has waited for the longest gains priority to get access to the locked resources. Condition SupportCondition objects specifying the signaling condition can be created.๐ก
Reentrancyis also a characteristic that is implicitly supported by thesynchronizedkeyword inJava, allowing athreadthat currently holds alockto re-acquire it without blocking itself. This built-in feature enablesthread-saferecursive method calls,nested synchronized methods, and modularisation without incurringdeadlocks.
A Reentrant Lock can be concisely implemented as below. For the implementation of the extra features, including Fairness and Conditions, please refer to the references below.
Reentrant Lock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// task execution over the shared resource...
} finally {
lock.unlock();
}
The below could be a good example of a deadlock and could be solved by chaning the order of a locks in either of one method, however not both.
public methodA(){
synchronized(lockA){
synchronized(lockB){
}
}
}
public methodB(){
synchronized(lockB){
synchronized(lockA){
}
}
}