💻Step 24: Multithreading-20th+21st hour + code

Learn with youtube video-

💡Definition-

Multithreading is a concept of execution of multiple threads, simultaneously/concurrently/at the same time.

💡Process-

A process is running state of instruction.


💡Thread-

Threads exist within a process.
every process has at least one thread in it.
Threads share the process’s resources,

Here is a comparison of processes and threads:

ProcessesThreads
A process is an independent execution unit that runs in its own memory space.A thread is a lightweight execution unit that runs within a process and shares the process’s memory space.
Each process has its own virtual address space, file handles, and other resources.Threads within the same process share these resources.
Creating a new process is more expensive than creating a new thread because it requires more resources and takes more time.Creating a new thread is faster than creating a new process because it requires fewer resources.
Processes are more isolated from each other than threads. If one process crashes, it will not affect other processes.If a thread crashes, it can cause the entire process to crash.
Processes are used for larger, independent tasks that do not need to communicate with each other frequently.Threads are used for smaller tasks that can run concurrently and communicate with each other more frequently.

💡Important –

1- Every Java program has at least one thread, the main thread, which is created by the Java Virtual Machine (JVM) when the program starts. You can use the currentThread() method to access the main thread and perform actions on it.

2- A daemon thread is a low-priority thread that runs in the background and does not prevent the program from exiting. An example of a daemon thread in the real world might be a thread that periodically checks for updates to a software application and installs them in the background.

💡Life Cycle of Thread-

·      

Here is a table that compares the different stages of a thread’s lifecycle in Java:

StageDescription
NewThe thread has been created, but start() has not been called on it.
Runnablestart() has been called on the thread, but it is not currently running. It is waiting for the Java runtime to schedule it for execution.
RunningThe thread is currently being executed by the CPU.
Non-Runnable (Blocked)The thread is not currently running and is not eligible to run. This can be because the thread is waiting for a resource, sleeping, or waiting for some other condition to be met.
TerminatedThe thread has completed execution or has been terminated prematurely due to an uncaught exception.
thrds

💡THREAD PRIORITY-

Threads with higher priority should be executed before lower-priority threads.
However, thread priorities cannot guarantee the order.

[Static fields]-
Java thread priorities are in the range between
1-MIN_PRIORITY (a constant of 1) and
2-MAX_PRIORITY (a constant of 10).
3-NORM_PRIORITY (a constant of 5)-By default.

💡Thread creation-

Here is a tabular comparison of the two approaches:

ComparisonExtending Thread classImplementing Runnable interface
How to create a new threadExtend the Thread class and override the run() methodImplement the Runnable interface and implement the run() method
How to start the new threadCall the start() method on the Thread objectCreate a new Thread object, passing an instance of your Runnable implementation to the Thread constructor, and then call the start() method
Can the same run() method be used by multiple threads?NoYes
Can the class extend other classes?NoYes

💡Q- which is better way?

A- In general, the Runnable interface is a more flexible approach, because a class can implement multiple interfaces but can only extend one superclass. However, the Thread class provides additional methods and fields for controlling and managing threads, which may be useful in some situations.

💡1) Using inheritance (Extending Thread class):

Note-

1- Always keep a separate class to start a main thread.

2- If you want to perform different task on a different thread then always keep a separate thread class for each task.

3- If you want to perform a common task on each thread then create a single thread class and make a multiple objects of same thread.

Example-

class Thread1 extends Thread {

public void run()

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

class Thread2 extends Thread {
public void run()
{
for(int i=0; i<=6; i++)
{
System.out.println(Thread.currentThread().getName());
}
}
}

class RunThread
{
public static void main(String[] Arg)
{

Thread1 t1 = new Thread1();

Thread2 t2 = new Thread2();

t1.start();

t2.start();
}
}

2) Using Association (Implementing Runnable interface):
implement Runnable and override the run method.

class Thread1 implements Runnable{

public void run() {

for(int i=0; i<=10; i++) {

System.out.println(Thread.currentThread().getName());

}

}

}

class RunThread
{
public static void main(String[] args)
{

Thread1 t1 = new Thread1();

Thread tt1 = new Thread(t1);

tt1.start();

Thread tt2 = new Thread(t1);

tt2.start();

}
}

💡Constructors in Thread class

Here is a tabular comparison of all the constructors of the Thread class in Java, along with code snippets showing how to use them:

ConstructorDescriptionCode snippet
Thread()Creates a new thread with no specific name (JVM provides default names) and no specific thread group.Thread t = new Thread();
Thread(String name)Creates a new thread with the given name and no specific thread group.Thread t = new Thread("Thread-1");
Thread(ThreadGroup group, String name)Creates a new thread with the given thread group and name.ThreadGroup group = new ThreadGroup("Group-1"); Thread t = new Thread(group, "Thread-1");
Thread(Runnable target)Creates a new thread with no specific name, no specific thread group, and the given Runnable object as its target.Runnable r = () -> System.out.println("Hello, World!"); Thread t = new Thread(r);
Thread(ThreadGroup group, Runnable target)Creates a new thread with no specific name, the given thread group, and the given Runnable object as its target.ThreadGroup group = new ThreadGroup("Group-1"); Runnable r = () -> System.out.println("Hello, World!"); Thread t = new Thread(group, r);
Thread(Runnable target, String name)Creates a new thread with the given name, no specific thread group, and the given Runnable object as its target.Runnable r = () -> System.out.println("Hello, World!"); Thread t = new Thread(r, "Thread-1");
Thread(ThreadGroup group, Runnable target, String name)Creates a new thread with the given thread group, name, and the given Runnable object as its target.ThreadGroup group = new ThreadGroup("Group-1"); Runnable r = () -> System.out.println("Hello, World!"); Thread t = new Thread(group, r, "Thread-1");
Thread(ThreadGroup group, Runnable target, String name, long stackSize)Creates a new thread with the given thread group, name, the given Runnable object as its target, and the specified stack size.ThreadGroup group = new ThreadGroup("Group-1"); Runnable r = () -> System.out.println("Hello, World!"); Thread t = new Thread(group, r, "Thread-1", 1024L);

Note that in order to start a new thread, you need to call the start() method on the Thread object. For example:

Thread t = new Thread(r, "Thread-1");
t.start();

This will actually create a new thread and start it running. The thread will execute the run() method of the Runnable object passed to the constructor.

Note – ThreadGroup class is used to represent a group of threads. It allows you to manipulate a group of threads as a single entity, including setting priority, setting a UncaughtExceptionHandler, and suspending and resuming all threads in the group at once.

💡Static methods of Thread Class-

Here is a tabular comparison of all the static methods of the Thread class, along with code snippets demonstrating their usage:

MethodDescriptionCode Snippet
currentThread()Returns a reference to the currently executing thread object.Thread currentThread = Thread.currentThread();
yield()Causes the currently executing thread object to temporarily pause and allow other threads to execute.

That’s why we can use this method as instance method and can call on instances of thread class.
Thread.yield();
sleep(long millis)Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds.Thread.sleep(1000);
sleep(long millis, int nanos)Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds and nanoseconds.Thread.sleep(1000, 500000);
activeCount()Returns the number of active threads in the current thread’s thread group.int activeCount = Thread.activeCount();
enumerate(Thread[] tarray)Copies the current thread’s thread group into the specified array. If the array is too small to hold all the threads, the extra threads are silently discarded.Thread[] threads = new Thread[10]; Thread.enumerate(threads);
dumpStack()Prints the current thread’s stack trace to the standard error stream.Thread.dumpStack();

.           

💡Instance methods-

Here is a tabular comparison of all instance methods of the Thread class in Java, along with brief descriptions and code snippets showing how each method can be used:

Method NameDescriptionCode Snippet
start()Starts the thread by calling the run() method.Thread t = new Thread(); t.start();
run()The entry point for the thread. This method is called by the start() method.public void run() { // thread code goes here }
join()Waits for the thread to finish executing.Thread t = new Thread(); t.start(); t.join();
join(long millis)Waits for the thread to finish executing, or for the specified number of milliseconds to elapse, whichever comes first.Thread t = new Thread(); t.start(); t.join(1000);
isAlive()Returns true if the thread is currently executing, and false otherwise.Thread t = new Thread(); t.start(); boolean alive = t.isAlive();
setName(String name)Sets the name of the thread.Thread t = new Thread(); t.setName("My Thread");
getName()Returns the name of the thread.Thread t = new Thread(); String name = t.getName();
setPriority(int priority)Sets the priority of the thread.Thread t = new Thread(); t.setPriority(Thread.MAX_PRIORITY);
getPriority()Returns the priority of the thread.Thread t = new Thread(); int priority = t.getPriority();
interrupt()Interrupts the thread.Thread t = new Thread(); t.interrupt();
isInterrupted()Returns true if the thread has been interrupted, and false otherwise.Thread t = new Thread(); boolean interrupted = t.isInterrupted();

·   

💡-Single example to combine most useful methods –

public class ThreadMethodsExample {
    public static void main(String[] args) throws InterruptedException {
        // Create a new thread
        Thread thread = new Thread(() -> {
            try {
                // Sleep for 500 milliseconds
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // Print a message if the thread is interrupted
                System.out.println("Thread interrupted while sleeping");
            }

            // Print a message
            System.out.println("Hello from a new thread!");

            // Yield to other threads
            Thread.yield();
        });

        // Set the name of the thread
        thread.setName("My Thread");

        // Set the priority of the thread
        thread.setPriority(Thread.MAX_PRIORITY);

        // Set the thread as a daemon thread
        thread.setDaemon(true);

        // Start the thread
        thread.start();

        // Wait for the thread to finish
        thread.join();

        // Get the name of the thread
        String threadName = thread.getName();
        System.out.println("Thread name: " + threadName);

        // Get the priority of the thread
        int priority = thread.getPriority();
        System.out.println("Thread priority: " + priority);

        // Check if the thread is a daemon thread
        boolean isDaemon = thread.isDaemon();
        System.out.println("Is daemon thread: " + isDaemon);

        // Check if the thread is alive
        boolean isAlive = thread.isAlive();
        System.out.println("Is thread alive: " + isAlive);

        // Interrupt the thread
        thread.interrupt();
        System.out.println("Thread interrupted");
    }
}


💡Can we start a thread twice?

->No. After staring a thread, it can never be started again. If you does so, an IllegalThreadStateException is thrown.

For Example:
T1.start();
T1.start(); (exception)

💡Here is a comparison between synchronization and lock in Java:

SynchronizationLock
ConceptSynchronization is a mechanism that allows only one thread to access a shared resource at a time. It is used to prevent race conditions.Lock is a mechanism that provides more advanced and flexible locking operations than synchronization. It allows a thread to acquire a lock and release it at a later time.
How it worksSynchronization is implemented using the synchronized keyword. A block of code can be marked as synchronized by placing the synchronized keyword before the block. When a thread enters a synchronized block, it acquires the lock for the object that the block is associated with. When the thread exits the block, it releases the lock.Lock is implemented using the Lock interface and its implementing classes, such as ReentrantLock. A lock can be acquired by calling the lock() method and released by calling the unlock() method.
Pros– Simple to use– More flexible than synchronization
– Built-in support in the Java language– Allows more fine-grained control over locking
– Provides a way to interrupt a thread that is waiting to acquire a lock
Cons– Limited capabilities compared to lock– More complex to use than synchronization
– Does not allow for interrupting a thread that is waiting to acquire a lock
– Does not support fairness policies
When to use– When the locking requirements are simple and the overhead of using lock is not justified– When more advanced and flexible locking is needed, such as when implementing a fair lock or when interrupting a thread that is waiting to acquire a lock

It’s important to note that both synchronization and lock are used to achieve thread safety in Java. However, lock offers more advanced and flexible locking capabilities compared to synchronization.

💡Synchronization in java –

💡Synchronization- It’s a capability of control the access of multiple threads to any shared resource.

💡Types of thread synchronization-

Here is a tabular comparison between mutual exclusion, inter-thread communication, synchronized method, synchronized block, and static synchronization in Java:

TechniqueDescriptionExample or Code Snippet
Mutual ExclusionIt’s concept where threads don’t interfere with one another while sharing data.
It ensures that only one thread can execute a critical section of code at a time.
synchronized keyword
Inter-Thread CommunicationAllows threads to communicate with each other and coordinate their actions.wait() and notify() methods
Synchronized MethodsA method that is declared synchronized can only be executed by one thread at a time. All other threads will be blocked.
Applies to the entire method
public synchronized void foo() { ... }
Synchronized BlocksA block of code that is surrounded by a synchronized block can only be executed by one thread at a time.
Only applies to the block of code within the curly braces
synchronized (obj) { ... }
Static SynchronizationA method or block of code that is static and synchronized can only be executed by one thread at a time.
Applies to the entire class and all its instances
public static synchronized void foo() { ... } or synchronized (MyClass.class) { ... }

1- Mutual exclusion example- (synchronized keyword- ): (Synchronized method example)

Here is a complete example of mutual exclusion in Java using the synchronized keyword:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

In this example, the increment and getCount methods are both marked with the synchronized keyword. This ensures that only one thread can execute either of these methods at a time.

Here is an example of how you might use this Counter class in a multi-threaded environment:

Counter counter = new Counter();

Thread t1 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counter.increment();
    }
});

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counter.increment();
    }
});

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println(counter.getCount());  // 2000

In this example, two threads are created and each increments the counter 1000 times. Without the use of mutual exclusion, the final value of the counter may not be accurate, as both threads could be updating the counter simultaneously. However, by using the synchronized keyword, we ensure that only one thread can execute the increment method at a time, and the final value of the counter is guaranteed to be accurate.

2- Mutual exclusion example- (synchronized keyword- ): (Synchronized block example)

Here is an example of using a synchronized block in Java:

public class SynchronizedExample {
    private int count = 0;

    public void increment() {
        synchronized(this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

In this example, the increment() method increments the count variable. However, the increment() method is surrounded by a synchronized block, which means that only one thread can execute the code inside the block at a time. This can be useful if multiple threads are accessing and modifying the count variable and you want to ensure that the variable is updated consistently and correctly.

To use a synchronized block, you first need to specify the object that you want to synchronize on. In this case, we are using this, which refers to the current object (i.e., the SynchronizedExample instance). You can also use any other object as the lock, as long as all threads that want to synchronize on the same block use the same lock object.

3- Mutual exclusion example- (synchronized keyword- ): (Static Synchronized example)

Here is an example of using a static synchronized method in Java:

public class SynchronizedExample {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

In this example, the increment() method is a static synchronized method, which means that only one thread can execute the method at a time. This can be useful if multiple threads are accessing and modifying the count variable and you want to ensure that the variable is updated consistently and correctly.

To use a static synchronized method, you simply need to use the synchronized keyword before the static keyword in the method declaration. The lock object used for the synchronized block is the class object of the class in which the synchronized method is defined. In this case, the lock object is the SynchronizedExample.class object.

4- Inter-thread communication example –

Here is an example of inter-thread communication in Java using the wait(), notify(), and notifyAll() methods from the Object class:

public class InterThreadCommunicationExample {

    public static void main(String[] args) {
        final Customer c = new Customer();
        new Thread() {
            public void run() {
                c.withdraw(15000);
            }
        }.start();

        new Thread() {
            public void run() {
                c.deposit(10000);
            }
        }.start();
    }
}

class Customer {
    int amount = 10000;

    synchronized void withdraw(int amount) {
        System.out.println("going to withdraw...");

        if (this.amount < amount) {
            System.out.println("Less balance; waiting for deposit...");
            try {
                wait();
            } catch (Exception e) {
            }
        }
        this.amount -= amount;
        System.out.println("withdraw completed...");
    }

    synchronized void deposit(int amount) {
        System.out.println("going to deposit...");
        this.amount += amount;
        System.out.println("deposit completed... ");
        notify();
    }
}

In this example, there are two threads: one for withdrawing money and one for depositing money. The Customer class has a synchronized withdraw() method and a synchronized deposit() method. The withdraw() method checks if there is enough balance to withdraw the requested amount. If there is not enough balance, it calls the wait() method to wait for a deposit.

The deposit() method deposits the given amount and then calls the notify() method to wake up the thread that is waiting in the withdraw() method.

The wait(), notify(), and notifyAll() methods can only be called from within a synchronized method or block. They are used to allow threads to communicate with each other and synchronize their actions.

💡Concept of Lock in java-

Synchronized is done around the concept of locks.

Objects have internal entity which is called monitors.

Threads can lock and unlock these monitors.

One object can hold only one lock.

One thread can hold many locks.

Here is a tabular comparison of the various locks in Java, along with some examples:

LockDescriptionExample
java.util.
concurrent.locks.Lock
A general-purpose lock interface that can be implemented by a wide range of lock classes.Lock lock = new ReentrantLock();
java.util.
concurrent.ReentrantLock
A reentrant mutual exclusion lock with the same basic behavior and semantics as the implicit monitor lock accessed using the synchronized keyword.ReentrantLock lock = new ReentrantLock();
java.util.concurrent.
ReentrantReadWriteLock
A lock that can be shared by multiple reader threads, but only exclusively acquired by a single writer thread at a time.ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

1- Here is an example of how to use a ReentrantLock in Java:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static int count = 0;
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            } finally {
                lock.unlock();
            }
        });

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            } finally {
                lock.unlock();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + count);
    }
}

In this example, two threads are incrementing a shared count variable concurrently. To ensure that the updates to the count variable are thread-safe, the threads acquire a lock on the ReentrantLock object before updating the count variable. The lock.lock() method acquires the lock, and the lock.unlock() method releases the lock.

Note that it is important to release the lock using the finally block to ensure that the lock is always released, even if an exception is thrown while the thread holds the lock.

2- Here is an example of using a ReentrantReadWriteLock in Java:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
  private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

  private int value;

  public int getValue() {
    readLock.lock();
    try {
      return value;
    } finally {
      readLock.unlock();
    }
  }

  public void setValue(int value) {
    writeLock.lock();
    try {
      this.value = value;
    } finally {
      writeLock.unlock();
    }
  }
}

This example defines a simple class that has a single integer value that can be read and written by multiple threads. The ReentrantReadWriteLock is used to ensure that the value is accessed in a thread-safe manner. The readLock is used to protect the getValue method, which can be called concurrently by multiple threads. The writeLock is used to protect the setValue method, which should only be called by a single thread at a time.

Interview Questions —

  • What is difference between process and thread?
  • What is life cycle of thread in java?

Leave a Reply

Your email address will not be published. Required fields are marked *