๐ชก
Thread
is a unit of a flow where resources are assigned by aprocess
.๐งต
Process
is 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 Class
Runnable Interface
Thread 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
deadlock
refers to a situation in computing where none of thethreads
orprocesses
can 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 safety
refers to the property inmulti-threaded programming
where afunction
,variable
, orobject
can be accessed bymultiple threads
simultaneously without causing the above issues (Synchronisation Problems
andDeadlock
) in the program's execution.More specifically, it is defined as a situation where, if a
function
is being called and executed by onethread
, anotherthread
can call and execute the samefunction
at 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 & Lock
ThreadLocal
where these will be discussed in greater detail in the following sections.
๐ A
synchronization
is a mechanism where access to the shared resource becomes restricted to only a singlethread
until 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 methods
synchronized 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
object
specified 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
.
ThreadLocal
is a mechanism that allows eachthread
to 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:
Reentrancy
thread
can acquire the same lock
it holds.Fairness
thread
that has waited for the longest gains priority to get access to the locked resources. Condition Support
Condition objects
specifying the signaling condition
can be created.๐ก
Reentrancy
is also a characteristic that is implicitly supported by thesynchronized
keyword inJava
, allowing athread
that currently holds alock
to re-acquire it without blocking itself. This built-in feature enablesthread-safe
recursive 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){
}
}
}