Java Multithreading
Java Multithreading
CPU
The CPU, often referred to as the brain of the computer, is responsible for
executing instructions from programs. It performs basic arithmetic, logic,
control, and input/output operations specified by the instructions.
Core
A core is an individual processing unit within a CPU. Modern CPUs can have
multiple cores, allowing them to perform multiple tasks simultaneously.
Program
Microsoft Word is a program that allows users to create and edit documents.
Process
A web browser like Google Chrome might use multiple threads for different
tabs, with each tab running as a separate thread.
Multitasking
Example: We are browsing the internet while listening to music and downloading
a file.
Multithreading
In a single-core system:
Both threads and processes are managed by the OS scheduler through time
slicing and context switching to create the illusion of simultaneous
execution.
In a multi-core system:
Both threads and processes can run in true parallel on different cores, with
the OS scheduler distributing tasks across the cores to optimize
performance.
Time Slicing
Definition: Time slicing divides CPU time into small intervals called time
slices or quanta.
Purpose: This allows multiple processes and threads to share the CPU,
giving the appearance of simultaneous execution on a single-core CPU or
improving parallelism on multi-core CPUs.
Multithreading in Java
Java provides robust support for multithreading, allowing developers to
create applications that can perform multiple tasks simultaneously,
improving performance and responsiveness.
The threads share the single core, and time-slicing is used to manage thread
execution.
The JVM can distribute threads across multiple cores, allowing true parallel
execution of threads.
When a Java program starts, one thread begins running immediately, which
is called the main thread. This thread is responsible for executing the main
method of a program.
To create a new thread in Java, you can either extend the Thread class or
implement the Runnable interface.
Method 1: extend the Thread class
1. A new class World is created that extends Thread.
2. The run method is overridden to define the code that constitutes the new
thread.
publicclassWorldextends Thread {
@Override
publicvoidrun() {
for(;;){
System.out.println("World");
}
}
}
2. The run method is overridden to define the code that constitutes the new
thread.
publicclassWorldimplements Runnable {
@Override
publicvoidrun() {
for(;;){
System.out.println("World");
}
}
}
Thread Lifecycle
The lifecycle of a thread in Java consists of several states, which a thread can
move through during its execution.
New: A thread is in this state when it is created but not yet started.
Runnable: After the start method is called, the thread becomes runnable.
It’s ready to run and is waiting for CPU time.
Running: The thread is in this state when it is executing.
Blocked/Waiting: A thread is in this state when it is waiting for a resource
or for another thread to perform an action.
Runnable vs Thread
Use Runnable when you want to separate the task from the thread, allowing
the class to extend another class if needed. Extend Thread if you need to
override Thread methods or if the task inherently requires direct control
over the thread itself, though this limits inheritance.
Thread methods
1. start( ): Begins the execution of the thread. The Java Virtual Machine
(JVM) calls the run() method of the thread.
2. run( ): The entry point for the thread. When the thread is started, the
run() method is invoked. If the thread was created using a class that
implements Runnable, the run() method will execute the run() method
of that Runnable object.
4. join( ): Waits for this thread to die. When one thread calls the join()
method of another thread, it pauses the execution of the current thread
until the thread being joined has completed its execution.
@Override
public void run() {
System.out.println("Thread is Running...");
for (int i = 1; i <= 5; i++) {
for (int j = 0; j < 5; j++) {
System.out.println(Thread.currentThread().getName() + " - Priori
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
}
}
publicclassMyThreadextends Thread {
@Override
public void run() {
while (true) {
System.out.println("Hello world! ");
}
}
publicstaticvoidmain(String[] args) {
MyThreadmyThread = new MyThread();
myThread.setDaemon(true); // myThread is daemon thread ( like Garbage co
MyThreadt1=new MyThread();
t1.start();//t1 is user thread
myThread.start();
System.out.println("Main Done");
}
}
Synchronisation
Let’s see an example where two threads are incrementing same couter.
class Counter {
private int count = 0; // shared resource
publicMyThread(Counter counter) {
this.counter = counter;
}
@Override
publicvoid run() {
for(int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
System.out.println(counter.getCount()); // Expected: 2000, Actual will b
}
}
The output of the code is not 2000 because the increment method in the
Counter class is not synchronized. This results in a race condition when both
threads try to increment the count variable concurrently.
Without synchronization, one thread might read the value of count before
the other thread has finished writing its incremented value. This can lead to
both threads reading the same value, incrementing it, and writing it back,
effectively losing one of the increments.
classCounter {
private int count = 0; // shared resource
By synchronizing the increment method, you ensure that only one thread
can execute this method at a time, which prevents the race condition. With
this change, the output will consistently be 2000.
Locks
The synchronized keyword in Java provides basic thread-safety but has
limitations: it locks the entire method or block, leading to potential
performance issues. It lacks a try-lock mechanism, causing threads to block
indefinitely, increasing the risk of deadlocks. Additionally, synchronized
doesn't support multiple condition variables, offering only a single monitor
per object with basic wait/notify mechanisms. In contrast, explicit locks
(Lock interface) offer finer-grained control, try-lock capabilities to avoid
blocking, and more sophisticated thread coordination through multiple
condition variables, making them more flexible and powerful for complex
concurrency scenarios.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
}
}
Reentrant Lock
A Reentrant Lock in Java is a type of lock that allows a thread to acquire the
same lock multiple times without causing a deadlock. If a thread already
holds the lock, it can re-enter the lock without being blocked. This is useful
when a thread needs to repeatedly enter synchronized blocks or methods
within the same execution flow. The ReentrantLock class from the
publicvoid innerMethod() {
lock.lock();
try{
System.out.println("Inner method");
}finally {
lock.unlock();
}
}
Methods of ReentrantLock
lock()
Acquires the lock, blocking the current thread until the lock is available.
It would block the thread until the lock becomes available, potentially
leading to situations where a thread waits indefinitely.
If the lock is already held by another thread, the current thread will wait
until it can acquire the lock.
tryLock()
Tries to acquire the lock without waiting. Returns true if the lock was
acquired, false otherwise.
This is non-blocking, meaning the thread will not wait if the lock is not
available.
tryLock(long timeout, TimeUnit unit)
Attempts to acquire the lock, but with a timeout. If the lock is not
available, the thread waits for the specified time before giving up. It is
used when you want to attempt to acquire the lock without waiting
indefinitely. It allows the thread to proceed with other work if the lock
isn't available within the specified time. This approach is useful to avoid
deadlock scenarios and when you don't want a thread to block forever
waiting for a lock.
Returns true if the lock was acquired within the timeout, false
otherwise.
unlock()
lockInterruptibly()
Acquires the lock unless the current thread is interrupted. This is useful
when you want to handle interruptions while acquiring a lock.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
writerThread.start();
readerThread1.start();
readerThread2.start();
writerThread.join();
readerThread1.join();
readerThread2.join();
Fairness of Locks
Fairness in the context of locks refers to the order in which threads acquire a
lock. A fair lock ensures that threads acquire the lock in the order they
requested it, preventing thread starvation. With a fair lock, if multiple
threads are waiting, the longest-waiting thread is granted the lock next.
However, fairness can lead to lower throughput due to the overhead of
maintaining the order. Non-fair locks, in contrast, allow threads to “cut in
line,” potentially offering better performance but at the risk of some threads
waiting indefinitely if others frequently acquire the lock.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
thread1.start();
thread2.start();
thread3.start();
}
}
Deadlock
A deadlock occurs in concurrent programming when two or more threads
are blocked forever, each waiting for the other to release a resource. This
typically happens when threads hold locks on resources and request
additional locks held by other threads. For example, Thread A holds Lock 1
and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Since
neither thread can proceed, they remain stuck in a deadlock state. Deadlocks
can severely impact system performance and are challenging to debug and
resolve in multi-threaded applications.
class Pen {
publicsynchronized void writeWithPenAndPaper(Paper paper) {
System.out.println(Thread.currentThread().getName() + " is using pen " +
paper.finishWriting();
}
class Paper {
public synchronized void writeWithPaperAndPen(Pen pen) {
System.out.println(Thread.currentThread().getName() + " is using paper "
pen.finishWriting();
}
@Override
publicvoid run() {
synchronized (pen){
paper.writeWithPaperAndPen(pen); // thread2 locks paper and tries to
}
}
}
thread1.start();
thread2.start();
}
}
Thread communication
class SharedResource {
private int data;
private boolean hasData;
public synchronized void produce(int value) {
while (hasData) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
d a t a = v a l u e ;
h a s D a t a = t r u e ;
S y s t e m . o u t . p r i n t l n ( " P r o d u c e d : " + v a l u
n o t i f y ( ) ;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
resource.produce(i);
}
}
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int value = resource.consume();
}
}
}
producerThread.start();
consumerThread.start();
}
}
Executors framework
The Executors framework was introduced in Java 5 as part of the
java.util.concurrent package to simplify the development of concurrent
applications by abstracting away many of the complexities involved in
creating and managing threads.
It will help in
2. Resource management
3. Scalability
4. Thread reuse
5. Error handling
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
}
executor.shutdown();
// executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
privatestaticlongfactorial(int n) {
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
throw new RuntimeException(e);
}
long result = 1;
for(inti=1;i<= n; i++) {
result *= i;
}
return result;
}
}
Future
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
publicclassMain {
Hello
null
Task is done !
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
Atomic classes
Volatile keyword
class SharedObj {
private volatile boolean flag = false;
writerThread.start();
readerThread.start();
}
}
CountDownLatch
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) throws InterruptedException {
int n = 3;
ExecutorService executorService = Executors.newFixedThreadPool(n);
CountDownLatch latch = new CountDownLatch(n);
executorService.submit(new DependentService(latch));
executorService.submit(new DependentService(latch)); executorService.submit(new
DependentService(latch)); latch.await(); System.out.println("Main");
executorService.shutdown();
}
}
Cyclic Barrier
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
webServerThread.start();
databaseThread.start();
cacheThread.start();
messagingServiceThread.start();
@Override
public void run() {
try {
System.out.println(name + " initialization started.");
Thread.sleep(initializationTime); // Simulate time taken to initiali
System.out.println(name + " initialization complete.");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}