Multithreading in Java

Multithreading in Java

Before started into let's understand multitasking

what is multitasking?

Multitasking involves the simultaneous execution of multiple processes, making efficient use of CPU resources. This capability allows a system to handle and manage several tasks concurrently, enhancing overall performance and leveraging the processing power of the CPU.

There are two types of Multitasking.

1.Process based Multitasking: It refers to the execution of independent process simultaneously.

2.Thread based Multitasking (Multithreading): It refers to the execution of multiple threads in one process concurrently, taking the advantage of multiple cores when available. Unlike process-based multitasking, multithreading operates within the same memory space, avoiding the allocation of separate memory allocations for each thread.

Multithreading in Java and Thread working

what is thread?

Thread is a Light-weight unit, it is a smallest independent part of a process. We can achieve the concurrency execution of a multiple threads with the process This concurrency is particularly advantageous in multi-core systems, leading to more efficient CPU utilization and time savings.

we can create a thread in two ways.

1. By extending the Thread Class

2. By implementing the Runnable interface

Thread Life Cycle

New: when a created a thread it will on new state

Runnable: When we call start(), then thread should Enter's into the Runnable state it means thread is ready for execution. this state, the thread awaits the thread scheduler to allocate processor time.

Running: In the 'Running' state, a thread is actively executing its tasks. However, if the thread needs to perform tasks such as I/O operations, it may temporarily transition to the 'Waiting' state. Additionally, if the thread's scheduled time limit is reached, it might be preempted, allowing other threads to run as the scheduler manages their execution.

Waiting state: In the 'Waiting' state, a thread awaits a specific action to be performed. While in this state, if a task is initiated on the thread, it transitions back to the 'Ready' state. The thread scheduler can then schedule the task for execution.

Dead: In this state, a thread has completed its task, typically when the run() method finishes execution, and it is terminated from the processor.

Extending the Thread Class:

Thread class will inherit from the object class.

package MulthiThreading;

public class m1 {
    public static void main(String[] args) {
        Mythread my = new Mythread();
        my.start();
        for (int j = 0; j < 4; j++) {
            System.out.println("parent thread");
        }
    }
}
class Mythread extends Thread{
    public  void run(){
        for(int i=0;i<4;i++){
                System.out.println("child thread");
        }
    }
}

2.By Implementing the Runnable Interface:

package MulthiThreading;

public class RunnableImpl {
    public static void main(String[] args) {
        MyThread r = new MyThread();  
        Thread t = new Thread(r);
        t.start();
        System.out.print("World");
    }
}
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.print("Hello");
    }
}

How are Threads Scheduled?

In Runnable state threads were scheduled by the Thread scheduler, it uses some algorithms that are first come first serves algorithm (FCFS) it basically our operating system will process the threads based on the arrival time and it is a non-preemptive, Time Slicing Scheduling it uses in multitasking where each thread will be executed with a certain time based it uses preemptive technique.it will check the Priority of each thread based on priority it schedules and it uses preemptive technique the priority start from 1 to 9 , highest number will get more priority.

Managing Thread Execution: Thread Control Methods

1.yield():This method is used to pause the current execution thread, allowing another thread to take over. The thread scheduler, based on its scheduling algorithms, determines which thread gets scheduled next.

package MulthiThreading;

public class yieldmethod {
    public static void  main(String[] args){
        Mythread1 my=new Mythread1();
        my.start();
        for(int j=0;j<5;j++){
            System.out.println("main thread");
        }

    }
}
class Mythread1 extends Thread{
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("child 1");
            Thread.yield();
        }

    }

}

2.Join(): This method is used when a thread wants to wait for the completion of another thread. Once the joined thread completes its execution, the waiting thread resumes its execution.

package MulthiThreading;

public class joinmethod {
    public static void  main(String[] args) throws InterruptedException{
       Mythread2 my=new Mythread2();
       my.start();
       my.join();
         for(int j=0;j<4;j++){
            System.out.println("main thread");
          }

    }
}
class Mythread2 extends Thread {
    public void run(){
        System.out.print("Hello");
        try {
           Thread.sleep(10000);
        }catch (Exception e){
             e.printStackTrace();
         }
    }
}

3. sleep() : This method is used to pause the execution of a thread for a specified amount of time. After the specified duration, the thread resumes its execution.

4.interrupt(): This method can be used to interrupt a thread that is in a sleeping or waiting state.

package MulthiThreading;


public class  Interputed{
    public static void main(String[] args) {
        Thread t = new Thread(new Thread1());
        t.start();
        t.interrupt();
    }
}
class Thread1 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Thread is going to sleep...");
            Thread.sleep(5000);
            System.out.println("Thread woke up!");
        } catch (InterruptedException e) {
            System.out.print("Interrputed");
           }
    }
}

Synchronization:

Synchronization in Java is a mechanism which involves in multiple threads access a shared resource concurrently. By utilizing synchronization, it ensures that only one thread can access the shared resource at any given time. This helps prevent data corruption and maintains consistency in scenarios where concurrent access could lead to conflicts or undesirable outcomes.

There are two types of thread synchronization.

1.Mutual Exclusive: it refers to only one thread will interact to particular resource without interfering other threads.

we can achieve the synchronization by using synchronized keyword, this is applicable methods, block, variables but not classes.

let's understand Lock concept: locks will handle by jvm, there are two types of locks object level and class-level if a thread wants to execute non-static synchronized method, then first it gets lock and it helps to prevent the another will not interact with the synchronized method after the thread execution finishes it release's the lock. And remaining threads will allow to execute non synchronized methods. and class-level lock is referred to static synchronized methods.

synchronized method:

package MulthiThreading;

public class Example {

    public static void main(String[] args) {
        ThreadExmaple t = new ThreadExmaple();
        t.start();  
        for (int i = 0; i < 3; i++) {
            System.out.print("parent ");
        }
    }
}

class ThreadExmaple extends Thread {
    public void run() {
        numbers(5);  
    }

    synchronized void numbers(int n) {
        for (int i = 0; i < n; i++) {
            System.out.println("child");
        }
    }
}

Inter-thread Communication

By this Inter-Thread Communication we can release the locks, by using wait(),notify(),notifyAll() methods

  • wait(): When a thread calls wait() on an object, it releases the lock associated with that object and enters a waiting state. The thread remains in the waiting state until another thread calls notify() or notifyAll() on the same object, releasing the waiting thread.

  • notify(): This method is used to wake up one of the threads that are waiting on the object. It does not specify which thread will be awakened. The awakened thread will compete with other threads for the lock and continue its execution.

  • notifyAll(): This method wakes up all the threads that are waiting on the object. All the awakened threads will compete for the lock.

Daemon Thread:

the thread which is running background of the process which the thread have low priority and it is used for Garbage collection bases on the conditions it runs as high priority like when memory is loaded full then to clean of some resources this thread will perform to cleanup.

Whenever User thread got finishes the execution then Jvm does not care about whether the daemon thread is running or not, it will terminate it whenever the user thread got executed.

isDaemon()- tocheck the thread is Daemon or not, to set the thread as daemon the use setDaemon(true).

package MulthiThreading;

public class DameonProgram {
    public static void main(String[] args) {
        ThreadDameon t = new ThreadDameon();
        t.setDaemon(true);
        t.start();

        for (int i = 0; i < 2; i++) {
            System.out.println(i+" " + Thread.currentThread().getName());
        }
    }
}

class ThreadDameon extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i+"" + Thread.currentThread().getName());
            System.out.println("is deamean "+Thread.currentThread().isDaemon());
        }
    }
}

DeadLock:

Deadlocks typically occur in multithreaded programs when two or more threads are blocked forever, each waiting for the other to release a resource. This can happen due to the way locks are acquired and released in a program.

Enhancement in the Multithreading

Thread Group:

A ThreadGroup in Java is a way to group multiple threads together. It provides a convenient way to manage and manipulate a set of threads as a single unit. so that we can perform common operations easily.

It is present in the java.lang.package, and every thread group is a child of the System, where system is the root thread group for all the threads in java, every thread belongs to some thread group, where main thread belongs to main group.

ThreadGroup p=new ThreadGroup(String name); 
ThreadGroup c=new ThreadGroup(p,"group name");

which creates a new thread group with the specified group name. the parent of this new group is the thread group of currently executing thread. It specifies that the g1 is the parent of group of the new thread group.

In java.util.concurrent.* package we have the Lock Interface and ReentrantLock which is implement the Lock.

Locks:

"This is similar to traditional implicit locks, but it provides more extensive operations than traditional implicit locks. Important methods include:

  • Lock Interface: It is an interface acquired by a thread to execute synchronized methods or blocks.

  • Methods:

    • tryLock(): It returns a boolean value. If the lock is available, it returns true. If the lock is not available, the thread continues its execution without waiting, and it does not enter the waiting state.

ReentrantLock:

It is the implementation class of the Lock interface and is a direct child class of the Object class. It allows a thread to acquire the same lock multiple times. Whenever we call the lock method multiple times, internally it increases the count of the threads. If we then call the unlock method, it decreases the count. When the count reaches zero, the lock will be released


ReentrantLock l=new ReentrantLock()
ReentrantLock l=new ReentrantLock(boolean fairness)
/*fairness  is true-> longest waiting thread get chance
it follows first come first serve policy
if fairness is false->then which waiting will get a chance we can't except 
default value is false
package MulthiThreading;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class LockUsingMethod {
    public static void main(String[] args) {
        ReentrantLock commonLock = new ReentrantLock();
        MyLock myLock1 = new MyLock(commonLock, "Thread 1");
        MyLock myLock2 = new MyLock(commonLock, "Thread 2");
        Thread thread1 = new Thread(myLock1);
        Thread thread2 = new Thread(myLock2);

        thread1.start();
        thread2.start();
    }
}

class MyLock implements Runnable {
    private final ReentrantLock sharedLock;
    private final String threadName;

    MyLock(ReentrantLock sharedLock, String threadName) {
        this.sharedLock = sharedLock;
        this.threadName = threadName;
    }

    @Override
    public void run() {
        try {
            if (sharedLock.tryLock(20000, TimeUnit.MILLISECONDS)) {
                   System.out.println(threadName + " - Inside Lock" + Thread.currentThread().getName());
                    try{
                        Thread.sleep(100);
                    }catch (Exception e){
                        e.printStackTrace();
                    }

            } else {
                System.out.println(threadName + " - Out of lock");
            }
            sharedLock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

ThreadPools(Executor framework):

This concept was introduced in Java 5. When we need a thread, creating a new thread every time can lead to performance issues and is not optimal for memory utilization. To address this problem, the ThreadPool concept was introduced. In a ThreadPool, instead of creating a new thread each time, we can utilize existing threads. After our job is complete, the thread goes back to the ThreadPool. This ThreadPool is also known as the Executor Framework.

The ExecutorService in Java is part of the java.util.concurrent package and provides a high-level replacement for working directly with threads. It manages and controls thread pools, allowing for the execution of tasks asynchronously.

When you're asked about different methods related to creating thread pools in the ExecutorService, you're probably being asked about the static factory methods provided by the Executors class. Besides newFixedThreadPool, here are other key methods:

1. newFixedThreadPool(int nThreads)

  • Creates a thread pool with a fixed number of threads. If all threads are busy, new tasks will wait in the queue until a thread becomes available.

  • Example: ExecutorService executor = Executors.newFixedThreadPool(5);

2. newCachedThreadPool()

  • Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when available. Threads that have not been used for a while are terminated and removed from the pool.

  • Example: ExecutorService executor = Executors.newCachedThreadPool();

3. newSingleThreadExecutor()

  • Creates a thread pool with only a single thread. This is useful when you need to ensure that tasks are executed sequentially in a single thread.

  • Example: ExecutorService executor = Executors.newSingleThreadExecutor();

4. newScheduledThreadPool(int corePoolSize)

  • Creates a thread pool that can schedule commands to run after a given delay or execute periodically. This is useful for scheduling tasks at fixed intervals.

  • Example: ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

5. newWorkStealingPool(int parallelism) (Java 8+)

  • Creates a thread pool that maintains enough threads to support the given parallelism level and uses a work-stealing algorithm to balance tasks between threads.

  • Example: ExecutorService executor = Executors.newWorkStealingPool();

6. newSingleThreadScheduledExecutor()

  • Similar to newSingleThreadExecutor() but with scheduling capabilities. It allows you to schedule commands to run after a delay or at a fixed rate in a single thread.

  • Example: ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

These methods give you various options for managing the execution of tasks depending on your use case (e.g., fixed-size pools, dynamic pools, scheduling tasks).

 ExecutorService service = Executors.newFixedThreadPool(int size);
//it will create a threadpool of size we can customize the sizes

service.submit(job) //this will start the thread execution 
serice.shutdown()//this will shutdown our threadpool
package MulthiThreading;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolConcept {
    public static void main(String[] args){
        ExecutorService service = Executors.newFixedThreadPool(3);
        MyThreadClass[] mythread={ new MyThreadClass("Hello"),
                new MyThreadClass("Programer"),
                new MyThreadClass("you"),
                new MyThreadClass("Understand"),
                new MyThreadClass("ThreadPool?"),

        };
        for(MyThreadClass t:mythread){
            service.submit(t);
        }
        service.shutdown();


    }
}
class MyThreadClass implements Runnable{

    private  String name;

    MyThreadClass(String name){
        this.name=name;
    }
    @Override
    public void run() {
        System.out.println(name+ "  "+Thread.currentThread().getName());
    }
}

Callable and Future:

The Callable interface was introduced in Java 1.5 and is akin to the Runnable interface. It allows us to create a thread using the Callable interface, which defines a single method called call(). This method returns an object.

The Future interface is employed to capture the value returned by the call() method. When we submit a Callable object to an executor, upon completion of the task, the thread returns an object of the type Future holding the result.

package MulthiThreading;

import java.util.concurrent.*;

public class ClallbleProgram {
    public static void main(String[] args)  {
        ExecutorService service = Executors.newFixedThreadPool(3);
        MyCallable[] mythread={ new MyCallable(1),
                new MyCallable(2),
                new MyCallable(3),
                new MyCallable(4),
                new MyCallable(5),

        };
        for(MyCallable t:mythread){
            Future<Integer> result = service.submit(t);
            try {
                System.out.println(result.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        service.shutdown();


    }
}
class MyCallable implements Callable {

    private  int  val;
    int sum=0;

    MyCallable(int  val){
        this.val=val;
    }

    @Override
    public Object call() throws Exception {
        for(int i=0;i<val;i++){
            sum+=i;
        }
        return sum;
    }
}

Producer and Consumer Problem:

Problem Overview

  1. Scenario:

    • Producers generate data and place it in a shared resource (queue or buffer).

    • Consumers retrieve data from the shared resource to process.

    • The shared resource has limited capacity, meaning it can't hold unlimited data.

  2. Challenges:

    • Synchronization: Multiple threads accessing a shared resource can lead to race conditions.

    • Blocking: If the buffer is full, producers should wait. If the buffer is empty, consumers should wait.

    • Deadlocks: Improper synchronization can lead to deadlocks where threads are stuck waiting indefinitely.

    • Efficient Resource Utilization: Ensuring both producers and consumers are active without unnecessary delays.