Process
The process is a program in execution and multiple processes communicate with each other via socket, signal handler, shared memory, semaphore, and files.
Thread
Thread is generally defined as a lightweight process that allows multiple activities in a single process.
In OOPs analogy if everything in the world is an object then thread is a soul that gives life to the objects.
Each thread has a program counter ( to track the currently executing instruction), stack, and local variables.
Thread Implementation
In Java to create a thread first, we need to define a task that needs to be executed in the thread. The task is defined with a class which implements the Runnable interface and override the run method.
public class PrintTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
Now create a thread with Thread class which takes a task to run.
public static void main(String[] args) {
Thread t1 = new Thread(new PrintTask());
t1.start();
}
Upon executing the start() method on the thread, JVM will ask the operating system to create a new thread and will execute the run method of the task in that particular thread.
In Java8 and the above, the task of the thread can be created via a lambda expression eg.
new Thread( () -> {
System.out.println(Thread.currentThread().getName());
}).start();
Terminology
Shared: A variable is considered as shared if it can be accessed by multiple threads.
Mutable: A variable whose values can be changed during its lifetime.
Thread Safety
If more than one thread is accessing a state variable and one or more threads are changing the state of the variable, it can bring the system to an incorrect state because of the race condition. It should be handled with synchronization.
If multiple threads are trying to access the same mutable state variable without appropriate synchronization then your code is BROKEN. Followings are the ways to fix the problem
Don’t share the state across the threads.
Make state variables immutable.
Use synchronization whenever accessing the sharable state variable.
A program is considered thread-safe If it behaves correctly when accessed from multiple threads regardless of the interleaving of the execution of those threads. Stateless objects are always thread-safe.
A program is written with the thread-safe class need not be a thread-safe and conversely, a thread-safe program can have non-thread-safe classes.
Race condition
Example of Read-Modify-Write
public class Counter {
private int count = 0;
public void incremenet() {
this.count++;
}
}
In the above example, two threads at the same time read the value of a variable count let say it be 4 and then both incremented it by 1 independently, and both save back the result at 5. Albeit, the correct result should have been 6 as the method can be called two times.
Example 2:
The race condition is when the order of execution impacts the correctness of the result.
Lazy Initialization
Example of Check-Then-Act
public class ExpensiveObject {
private static ExpensiveObject instance = null;
public static ExpensiveObject getInstance() {
if (instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
In this scenario as well, two threads A & B can check at the same time if the instance is null. Both threads will be resulting in creating more than 1 expensive object and defeat the purpose of the above logic.
Compound Operations
Operations that are impacting the mutable states should be atomic in nature. It means if one thread is modifying the state, then the state should be visible to other threads either before or after the operation not in between.
The above-discussed problem of Read-Write-Modify can be solved using atomic variables ( java.util.concurrent.atomic )
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void incremenet() {
this.count.incrementAndGet();
}
}
It’s a good solution if there is only a single state of the class, but generally, we have more than one state in each class and these states are interdependent too.
Locking
Java provides a built-in mechanism for enforcing atomicity: the Synchronized block.
Basic Syntax:
synchronized(lock) { // compound operations that intended to be atomic with respect to the other threads.
}
Every object can act as the lock for the purpose of synchronization. These are known as intrinsic locks.
Analogy, consider synchronized block as a room in which a person (thread) enters and locks the room. No other person(thread) can enter because the room is locked. When the first-person come out he puts the key back which can be taken by any one person and then he can enter the room.
A thread can come out via a normal path of execution or by throwing an exception.
Intrinsic lock act the Mutex (Mutual Exclusion), a lock at a time can be acquired by almost one thread. If a thread B tries to acquire a lock which is already acquired by thread A, then the thread B will be blocked until thread A release the lock. If thread A never releases the lock, then B will be blocked forever.
The synchronized block will be executed as a single atomic unit relative to the other threads.
Example:
void synchronized increment() {
count = count + 1;
}
In the above example, the whole method is being synchronized. We haven’t explicitly specified a lock-in but implicitly it will take a lock over the object on which this method will be called. If a method is static then it will take lock over the class.
Ideally synchronizing the whole method is not a good practice as it reduces the concurrency of the application. We should only synchronize the critical section.
Example:
void incrementSync() {
synchronized (this) {
count = count + 1;
}
}
Reentrancy
If the same thread retries to enter the synchronized block again, it will be able to enter because the lock is at the thread level not at the invocation level.
Analogy, Person holding the key to the room can enter the room as many times he wants.
Example:
public class Base {
public synchronized void doSomething() {
}
}
public class Derived extends Base {
public synchronized void doSomething() {
System.out.println("In derived class");
super.doSomething();
}
}
If reentrancy is not allowed, then if a derived class synchronized method will call the superclass synchronized method it would have resulted in a deadlock.
Guarding states with locks to make the compound operations atomic to avoid race conditions:
Read-modify-write
Check-then-act
Just guarding the write state is not enough, we should have the synchronization with the same lock at every place where the state is being accessed. It is a common misconception that lock should be added where the state is being written. Synchronizing all access to state variables with lock is known as a guard by the lock.
Each object provides the locking capability so that you don’t need to create a lock explicitly. Though there is no relation between the object’s lock and its states.
Liveliness and Performance
If synchronized completed service, then only one thread will be able to execute at one time which defeated the purpose of having a multithreading environment and resulted in a poor concurrent application. You should narrow down the scope of synchronization to critical sections only.
Avoid holding locks during long length computation operations e.g. Network or Console I/O.
Source: Medium
The Tech Platform
Comments