Multi Threading
Multi Threading
Core
Program
Process
Thread
A web browser like Google Chrome might use multiple threads for different
tabs, with each tab running as a separate thread.
Multitasking
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.
Function: The OS scheduler allocates these time slices to different
processes and threads, ensuring each gets a fair share of CPU time.
Purpose: This prevents any single process or thread from monopolizing the
CPU, improving responsiveness and enabling concurrent execution.
Context Switching
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.
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.
Terminated: A thread is in this state when it has finished executing.
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("RUNNING"); // RUNNING
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
Thread methods
start( ): Begins the execution of the thread. The Java Virtual Machine
(JVM) calls the run() method of the thread.
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.
sleep(long millis): Causes the currently executing thread to sleep
(temporarily cease execution) for the specified number of milliseconds.
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.
setPriority(int newPriority): Changes the priority of the thread. The
priority is a value between Thread.MIN_PRIORITY (1) and
Thread.MAX_PRIORITY (10).
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@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() + " -
Priority: " + Thread.currentThread().getPriority() + " - count: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
6. interrupt(): Interrupts the thread. If the thread is blocked in a call
to wait(), sleep(), or join(), it will throw an InterruptedException.
class Counter {
private int count = 0; // shared resource
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
System.out.println(counter.getCount()); // Expected: 2000, Actual
will be random <= 2000
}
}
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.
class Counter {
private int count = 0; // shared resource
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;
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()
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();
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 {
public synchronized void writeWithPenAndPaper(Paper paper) {
System.out.println(Thread.currentThread().getName() + " is using
pen " + this + " and trying to use paper " + paper);
paper.finishWriting();
}
@Override
public void run() {
pen.writeWithPenAndPaper(paper); // thread1 locks pen and tries
to lock paper
}
}
@Override
public void run() {
synchronized (pen){
paper.writeWithPaperAndPen(pen); // thread2 locks paper and
tries to lock pen
}
}
}
thread1.start();
thread2.start();
}
}
Thread communication
class SharedResource {
private int data;
private boolean hasData;
@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
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);
}
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;
}
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;
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
initialize
System.out.println(name + " initialization complete.");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}