Multi Threading
Multi Threading
Introduction:-
Multithreading is a programming concept that allows a process to create and manage multiple
threads of execution concurrently within the same program. It enables tasks to run in parallel,
improving the overall performance and efficiency of a program by utilizing the full power of multi-
core processors.
Key Concepts
1. Thread: A thread is the smallest unit of a process that can be scheduled by the
operating system. Each thread in a process shares the same memory space but can
execute independently.
2. Process vs Thread:
o A process is an instance of a program that runs in its own memory space.
o A thread is a subset of a process, and multiple threads can exist within the
same process, sharing resources like memory and file handles.
3. Concurrency vs Parallelism:
o Concurrency is when multiple tasks make progress within overlapping time
periods. It doesn’t necessarily mean they are running at the same time.
o Parallelism is when multiple tasks are executed simultaneously on different
processors or cores.
4. Thread Life Cycle:
o New: Thread is created but not yet started.
o Runnable: Thread is ready to run and waiting for CPU time.
o Running: Thread is actively executing instructions.
o Blocked/Waiting: Thread is waiting for a resource to become available.
o Terminated: Thread has finished execution.
Benefits of Multithreading
Use Cases
In Java, a thread can be defined in multiple ways. Here are the two most common
approaches:
You can create a thread by defining a class that extends the Thread class and overriding its
run() method.
Example:
Another way to define a thread is by implementing the Runnable interface and passing an
instance of the class to a Thread object.
Example:
class MyRunnable implements Runnable {
public void run() {
// Code that defines the behavior of the thread
System.out.println("Thread is running");
}
}
In Java, you can get and set the name of a thread using the getName() and setName()
methods provided by the Thread class.
You can retrieve the name of a thread using the getName() method.
Output:
Thread running: MyCustomThread
Example 2: Setting and Getting the Name Using setName() and getName()
Output:
Thread name before running: InitialThread
Thread running: WorkerThread
Key Points:
Thread priorities :
In Java, thread priorities determine the relative importance of threads when they are competing for
CPU time. Each thread is assigned a priority, and the thread scheduler uses these priorities to
determine which thread should run next. However, thread scheduling behavior can vary across
different platforms and JVM implementations, so thread priority does not guarantee execution order.
Key Concepts:
1. Thread Priority Range: Java thread priorities are integers between 1 and 10:
o Thread.MIN_PRIORITY = 1 (lowest priority)
o Thread.NORM_PRIORITY = 5 (default priority)
o Thread.MAX_PRIORITY = 10 (highest priority)
2. Setting Thread Priority: You can set a thread's priority using the setPriority()
method:
3. Getting Thread Priority: You can get the current priority of a thread using the
getPriority() method:
int priority = thread.getPriority();
4. Thread Scheduling:
Threads with higher priorities are more likely to be scheduled for execution
compared to those with lower priorities.
Time-slicing: Even though a thread with a higher priority might be favored, it doesn’t
necessarily mean that a lower-priority thread will not execute. The operating system
and JVM implement time-slicing mechanisms.
Example:
t1.start();
t2.start();
t3.start();
}
}
Important Points:
Thread priorities may not behave as expected across different JVMs and operating
systems, so it's not a good practice to rely heavily on priorities for thread
management.
Java provides higher-level concurrency tools, such as Executors and Locks, which
are generally preferred over managing thread priorities directly.
In Java, you can prevent thread execution in several ways depending on the scenario. Here are some
common methods:
1. Using Thread.sleep()
You can make a thread sleep for a specified amount of time, effectively preventing its execution for
that period.
try {
Thread.sleep(1000); // Pauses execution for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
2. Using Thread.yield()
The yield() method hints to the thread scheduler that the current thread is willing to yield
its current use of a processor. It doesn't prevent execution, but it gives other threads a
chance to execute.
Thread.yield(); // Allows other threads to execute
3. Using join()
The join() method can be used to stop the execution of the current thread until another
thread finishes its execution.
Thread t1 = new Thread(() -> {
// Task for t1
});
t1.start();
try {
t1.join(); // Current thread waits until t1 finishes
} catch (InterruptedException e) {
e.printStackTrace();
}
Synchronization :
In Java, synchronization is a process used to control the access of multiple threads to shared
resources. Without synchronization, it’s possible for multiple threads to access and modify
shared data concurrently, which can lead to inconsistent or incorrect results. Java provides
several mechanisms to implement synchronization, helping ensure that only one thread can
access a resource at a time.
synchronized (this) {
// Critical section code
}
2. Synchronized Method: You can make an entire method synchronized by using the
synchronized keyword in the method declaration. The object lock is automatically
acquired when the method is called, and other threads must wait until the lock is released.
// method code
3. Static Synchronized Method: Static methods can also be synchronized, but the lock is on
the class object (Class), not on an instance of the class. This means that only one thread
can execute a static synchronized method across all instances of the class.
// method code
Example:
class Counter {
return count;
counter.increment();
});
counter.increment();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
In the above example, the increment method is synchronized, so the shared resource count is
accessed by only one thread at a time. This ensures that the final value of count is correct after both
threads complete their execution.
System.out.println(Thread.currentThread().getName() + " is
running");
// Calling yield() to give a hint to the scheduler
Thread.yield();
}
}
thread1.start();
thread2.start();
}
}
Join method :
package lembda;
thread1.start();
thread1.join(); // Main thread waits for thread1 to
finish
thread2.start();
thread3.start();
thread2.join(); // Main thread waits for thread2 to
finish
thread3.join(); // Main thread waits for thread3 to
finish
Syncronized block :
package lembda;
class Counter {
private int count = 0;
thread1.start();
thread2.start();
class Counter1 {
private int count = 0;
MyThread(Counter counter) {
this.counter = counter;
}
t1.start();
t2.start();
Inter-thread communication in Java is typically done using methods like wait(), notify(),
and notifyAll() from the Object class. These methods help threads communicate by
allowing one thread to pause its execution until another thread notifies it. This is often used in
scenarios like producer-consumer problems.
Let's consider a simple example where a producer thread generates a value, and a consumer
thread consumes that value. The consumer has to wait until the producer provides the value.
class SharedResource {
private int value;
private boolean hasValue = false;
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
resource.produce(i);
try {
Thread.sleep(1000); // simulate time to produce
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
resource.consume();
try {
Thread.sleep(1500); // simulate time to consume
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
producerThread.start();
consumerThread.start();
}
}
Explanation:
SharedResource: This class holds the shared value between producer and consumer
threads. It has methods to produce and consume the value.
produce(): The producer thread calls this method. If a value has already been
produced (i.e., hasValue == true), the thread waits until the consumer consumes it.
consume(): The consumer thread calls this method. If no value has been produced yet
(i.e., hasValue == false), the thread waits until the producer generates a value.
wait(): This causes the current thread to wait until another thread calls notify() on
the same object.
notify(): Wakes up a single thread that is waiting on this object's monitor.
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5
This example shows how one thread can wait for another to finish a task before proceeding,
demonstrating basic inter-thread communication in Java.
DeadLock :
A deadlock in Java occurs when two or more threads are blocked forever, waiting for each
other to release resources. This happens when multiple threads hold locks and attempt to
acquire locks held by other threads, forming a circular dependency.
class A {
}
synchronized void last() {
System.out.println("Inside A.last()");
class B {
System.out.println("Inside B.last()");
A a = new A();
B b = new B();
DeadlockDemo() {
t.start();
new DeadlockDemo();
package lembda;
class DeadlockExample {
// Two resource objects
private final Object resource1 = new Object();
private final Object resource2 = new Object();
synchronized (resource2) {
System.out.println("Thread 1: Locked
resource 2");
}
}
}
try {
// Simulating some work with sleep
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2: Locked
resource 1");
}
}
}
t1.start();
t2.start();
}
}
Explanation:
Thread 1 (main thread) holds the lock on object A and tries to get the lock on object
B.
Thread 2 (child thread) holds the lock on object B and tries to get the lock on object
A.
Since neither thread can acquire the lock held by the other, both are stuck, and this
causes a deadlock.
1. Avoid nested locks: Try to avoid locking multiple resources at the same time.
2. Use a timeout for locks: This can allow the program to continue if a thread can’t
acquire a lock within a specified time.
3. Lock ordering: Always acquire locks in a consistent order.
Demon Thread :
A daemon thread in Java is a type of thread that runs in the background and performs tasks
like garbage collection, memory management, or other background tasks. Unlike regular
threads, daemon threads are terminated by the JVM when all user (non-daemon) threads have
finished execution. Daemon threads are not meant to keep the program running.
You can mark a thread as a daemon thread using the setDaemon(true) method before
starting the thread. Here's an example:
if (Thread.currentThread().isDaemon()) {
System.out.println(Thread.currentThread().getName() + " is a
daemon thread.");
} else {
System.out.println(Thread.currentThread().getName() + "
executing: " + i);
try {
} catch (InterruptedException e) {
e.printStackTrace();
t1.start();
t2.start();
Output:
Thread-0 executing: 0
Thread-1 executing: 0
Thread-0 executing: 1
Thread-1 executing: 1
Thread-0 executing: 2
Thread-1 executing: 2
Thread-0 executing: 3
Thread-1 executing: 3
Thread-0 executing: 4
Explanation:
Thread-0 (a user thread) continues its execution even after the daemon thread
Thread-1 starts.
If the Thread-0 completes its execution, the JVM will terminate all daemon threads,
even if they haven't finished their work. So, in this case, the daemon thread Thread-1
might not print all its iterations, depending on the timing.
Key Points:
Daemon threads run in the background and are terminated by the JVM once all user
threads are finished.
Use the setDaemon(true) method to make a thread a daemon.
Daemon threads are useful for tasks like garbage collection, but they should not be
relied upon for critical tasks, as they may not complete if the JVM exits.
Multithreading enhancement
Lock :
In Java multithreading, a reentrant lock is a type of lock that allows a thread to enter a
synchronized block of code that is already locked by the same thread. It prevents the situation
where a thread would block itself because it already owns the lock.
1. Lock Acquisition: A thread can acquire the lock multiple times (reentrant). It must
release the lock the same number of times to fully unlock it.
2. Fairness: You can create a fair lock by passing true to the ReentrantLock
constructor. In a fair lock, threads acquire locks in the order they requested them,
which reduces the chance of starvation.
3. Interruptible Lock Acquisition: A thread can interrupt another thread waiting to
acquire the lock.
4. Try-Lock: Instead of waiting indefinitely, a thread can attempt to acquire the lock
and return immediately if it’s unavailable.
5. Condition Variables: ReentrantLock allows you to associate multiple Condition
objects with it, which can be used for more sophisticated wait/notify patterns.
Example usage:
import java.util.concurrent.locks.ReentrantLock;
t1.start();
t2.start();
}
}
The ReentrantLock class offers more control over synchronization, making it useful in
situations where the built-in synchronized keyword may be too limited.
Thread pool :
A Thread Pool in Java is a mechanism for managing multiple threads efficiently by reusing a
fixed number of threads to execute tasks. Instead of creating a new thread for every task, a
thread pool reuses existing threads to handle multiple tasks, which can improve performance
and reduce overhead.
Java provides an ExecutorService interface that facilitates thread pool management. The
most common implementation is through the ThreadPoolExecutor class or the Executors
utility class.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
this.taskName = name;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is
executing task: " + taskName);
try {
} catch (InterruptedException e) {
e.printStackTrace();
threadPool.execute(task);
Explanation:
Task 1 completed.
Task 2 completed.
Task 3 completed.
Task 4 completed.
Task 5 completed.
This example shows how tasks are assigned to the available threads in the pool. Once a
thread completes its current task, it moves on to the next one in the queue.
You can use other thread pool types like: