Threads in Java

·

11 min read

Life Cycle of a Thread

Inter-thread Communication in Java

  • Inter-thread communication or Co-operation is all about allowing synchronized threads to communicate with each other.

  • Inter-thread communication is a mechanism in which a thread is paused running in its critical section and another thread is allowed to enter (or lock) in the same critical section to be executed. It is implemented by following methods of Object class:

    • wait()

    • notify()

    • notifyAll()


Monitor Lock

  • In java monitor lock is the mechanism used to enforce mutual exclusion and synchronization between threads when accessing shared resources. It ensures only one thread at a time can execute a block of code or method that is synchronized on same monitor lock

Key Points about Monitor lock

  • In Java every object has its own associated monitor lock. This lock is used by threads to coordinate access to critical section of code.

  • A thread must acquire monitor lock of an object before entering a synchronized block or method.

  • A monitor lock is automatically released when :

    • The thread exists the synchronized block or method.

    • The thread calls wait() on the object (this releases the lock temporarily).

    • It is not released when thread is interrupted or goes in sleep() state.

Behavior in Multithreaded Context

  • If one thread holds the monitor lock:

    • Other threads trying to access the synchronized block or method will be blocked.

    • These threads remain in Blocked State until the lock is released.

  • If thread calls wait()

    • It releases the monitor lock temporarily and enters the waiting state.

    • It will need to re-acquire the monitor lock before resuming execution after being notified.


Synchronization in java

Synchronization in java and thread synchronization in java ...

Synchronization in Java is the process that allows only one thread at a particular time to complete a given task entirely. By default, the JVM gives control to all the threads present in the system to access the shared resource, due to which the system approaches race condition

  • Synchronized method in java
class Demo {
    public synchronized void access() {
        System.out.println("Hello from thread")    
    }
}

Methods for Inter-thread communication

1) wait() method

  • The wait() method causes current thread to release the lock and wait until either another thread invokes the notify() method or the notifyAll() method for this same object, or a specified amount of time has elapsed.

  • The current thread must own this object's monitor, so it must be called from the synchronized method only otherwise it will throw exception

2) notify() method

  • The notify() method wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation.

3) notifyAll() method

  • Wakes up all threads that are waiting on this object's monitor.

Difference between wait and sleep?

wait()sleep()
The wait() method releases the lock.The sleep() method doesn't release the lock.
It is a method of Object classIt is a method of Thread class
It is the non-static methodIt is the static method
It should be notified by notify() or notifyAll() methodsAfter the specified amount of time, sleep is completed.

Example 1 for Synchronization

  • SynchronizationDemo.java
package com.codefreak.thread;

public class SynchronizedDemo {

    public synchronized void task1() {
        try {
            System.out.println("Task 1 executing");
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void task2() {
        try {
            System.out.println("Task 2 executing");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void task3() {
        System.out.println("Task 3 executing");
    }
}
  • MainClass.java
public class MainClass { 
    public static void main(String[] args) {

        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

        Thread t1 = new Thread(() -> {
            synchronizedDemo.task1();
        });

        Thread t2 = new Thread(() -> {
            synchronizedDemo.task2();
        });

        Thread t3 = new Thread(() -> {
            synchronizedDemo.task3();
        });

        t1.start();
        t2.start();
        t3.start();
    }
}

Explanation

  • Here when thread starts execution any thread is invoked first based on jvm, as of now let’s consider t1→t2→t3 in this way they have been invoked by jvm.

  • First thread1 will execute its method and will go in timed waiting state for 5 second and it will execute whole task1 method.

  • Then thread3 will execute its method as it is not synchronized the lock will not be acquired.

  • Then thread2 will execute its method this is synchronized method so lock will be acquired by thread2.

Example 2 for Synchronization

  • SharedResource.java
package com.codefreak.thread;

public class SharedResource {

    private List<Integer> items;
    private boolean itemAvailable;

    public SharedResource() {
        this.items = new ArrayList<Integer>();
        this.itemAvailable = false;
    }

    public synchronized void addItem(int item) {
        try {
            items.add(item);
            itemAvailable = true;
            notifyAll();
            System.out.println("Procuder thread added item " + item);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void getItem() {
        try {
            while (!itemAvailable) {
                System.out.println("Consumer thread going in wating state");
                wait(); // releases the lock
            }
            System.out.println("Consumer thread Printing item");
            for (int item : items) {
                System.out.print(item + " -> ");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • MainClass.java
public class MainClass {
    public static void main(String[] args) {

        SharedResource sharedResource = new SharedResource();

        Thread producerThread = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {}
            sharedResource.addItem(10);
        });

        Thread consumerThread = new Thread(() -> {
            sharedResource.getItem();
        });

        producerThread.start();
        consumerThread.start();
    }
}

Important Point to Note:

  • A thread does not immediately acquire a lock when it starts. It will only acquire a lock when it enters a synchronized method or block, and it will only acquire the lock if no other thread is holding it.

  • If the lock is already help by other thread then the thread will be blocked until it acquires a lock.

Example:

  • MainClass.java
public class MainClass {
    public static void main(String[] args) {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronizedDemo.task1();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronizedDemo.task2();
            }
        });

        t1.start();
        t2.start();
    }
}
  • SynchronizedDemo.java
package com.codefreak.thread;

public class SynchronizedDemo {

    public synchronized void task1() {
        System.out.println("Inside task 1");
        try {
            System.out.println("--Task 1 executing---");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Exiting Task 1");
    }

    public synchronized void task2() {
        System.out.println("Inside task 2");
        try {
            System.out.println("---Task 2 executing---");
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Exiting Task 2");
    }
}
Inside task 1
--Task 1 executing---
Exiting Task 1

Inside task 2
---Task 2 executing---
Exiting Task 2

Inside task 2
---Task 2 executing---
Exiting Task 2

Inside task 2
---Task 2 executing---
Exiting Task 2

Inside task 2
---Task 2 executing---
Exiting Task 2

Inside task 2
---Task 2 executing---
Exiting Task 2

Inside task 1
--Task 1 executing---
Exiting Task 1

Inside task 1
--Task 1 executing---
Exiting Task 1

Inside task 1
--Task 1 executing---
Exiting Task 1

Inside task 1
--Task 1 executing---
Exiting Task 1

What happens when Java Program is Run

  • When we start java program a JVM instance is created. This JVM instance is responsible for executing your program and managing its lifecycle.

What is JVM Instance?

  • A JVM instance is a runtime environment for executing a Java application.

  • Each Java application runs in its own isolated instance of the JVM.

  • The instance include component’s like memory areas, class loaders and thread management.

How is JVM Instance created ?

  1. When you execute a Java Program (e.g. using java MyProgram), the JVM Instance is started by the Java launcher (Java command).

    • The OS launches the JVM Process

    • The JVM allocates the memory and prepare runtime environment.

  2. Loading the Class

    • The JVM locates and loads the main class (The class containing public static void main (String[] args)) method.
  3. Starting Exectution

    • The JVM starts the main thread to execute the main method

    • The JVM manages the lifecycle of the threads (both user and daemon) and allocates memory for objects and classes.

Key Notes :

  • Each Java Program has its own isolated JVM Instance.

  • The JVM instance is what enables platform independence by executing the compiled bytecode (.class files).

  • A JVM instance is destroyed when the program finishes or explicitly terminates.

Custom Lock in Java Threads

  • In java, custom locks provide more advanced and flexible locking mechanisms compared to the synchronized keyword. The java.util.concurrent.locks package offers classes like ReentrantLock and ReentrantReadWriteLock that allows you to create custom locks.

  • As we know locks are object specific if a lock is acquired by a thread for that object then no other thread for that object can go in critical section/synchronized method but other threads on different object can go.

  • So how to overcome this thing? for this purpose we can use custom locks.

1). Reentrant Lock:

  • Reentrant lock is a lock that can be acquired multiple times by the same thread without causing a deadlock.

  • If we have two different object and two different threads and those threads are accessing same resource in a synchronized manner we can use reentrant lock, as we know if two different threads are there on separate object and they try to access same resource at same time they will be able to access it as thread acquire lock from their object as these are not same object separate lock for both of them will be acquired.

  • For this above problem we can use reentrant lock where we can share same lock instance across our shared resource so that they can acquire this lock.

Example:

  • SharedResource.java
package com.codefreak.thread.locks;

import java.util.concurrent.locks.Lock;


public class SharedResourceReentrant {

    private final Lock lock;

    public SharedResourceReentrant(Lock lock) {
        this.lock = lock;
    }


    public void producer() {
        lock.lock();
        try {
            System.out.println("Lock Acquired by: " + Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (Exception e) {
            // some exception
        }
        finally {
            System.out.println("Lock Releasing by : " + Thread.currentThread().getName());
            lock.unlock();
        }
    }
}
  • MainClass.java
public class MainClass {
    Lock sharedLock = new ReentrantLock();
    SharedResourceReentrant resource1 = new SharedResourceReentrant(sharedLock);
    Thread t1 = new Thread(() -> {
        resource1.producer();
    });

    SharedResourceReentrant resource2 = new SharedResourceReentrant(sharedLock);
    Thread t2 = new Thread(() -> {
        resource2.producer();
    });

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

ReentrantLock for same object with different method

  • SharedResourceReentrant.java
package com.codefreak.thread.locks;

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


public class SharedResourceReentrant {

    Lock myLock = new ReentrantLock();

    public void producer() {
        myLock.lock();
        try {
            System.out.println("Lock Acquired by: " + Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (Exception e) {
            // some exception
        }
        finally {
            System.out.println("Lock Releasing by : " + Thread.currentThread().getName());
            myLock.unlock();
        }
    }

    public void consume() {
        myLock.lock();
        try {
            System.out.println("Lock Acquired by: " + Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (Exception e) {
            // some exception
        }
        finally {
            System.out.println("Lock Releasing by : " + Thread.currentThread().getName());
            myLock.unlock();
        }
    }

}
  • MainClass.java
public class MainClass {
    public static void main(String[] args) {
        SharedResourceReentrant resource = new SharedResourceReentrant(sharedLock);
        Thread t1 = new Thread(() -> {
            resource.producer();
        });

        Thread t2 = new Thread(() -> {
            resource.consume();
        });

        t1.start();
        t2.start();
    }
}
  • output

2). Read/Write Lock

  • ReadWrite Lock allows multiple threads to read shared data simultaneously while ensuring that only one thread can write to the shared data at a time.

Key Components of ReadWriteLock

  1. ReadLock:

    • More than 1 thread can acquire read lock.

    • Allows multiple threads to read simultaneously as long as no thread is writing.

    • Blocks if there is an active writer.

  2. WriteLock:

    • Only 1 thread can acquire write lock.

    • Blocks all the readers and writers while the lock is held.

The most common implementation of ReadWriteLock is ReentrantReadWriteLock.

Example:

  • SharedResourceReadWrite.java
package com.codefreak.thread.locks;

import java.util.concurrent.locks.ReadWriteLock;

public class SharedResourceReadWrite {

    public void produce(ReadWriteLock readWriteLock) {
        try {
            readWriteLock.readLock().lock();
            System.out.println("ReadLock acquired by : " + Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (Exception e) {} 
        finally {
            System.out.println("ReadLock releasing by : " + Thread.currentThread().getName());
            readWriteLock.readLock().unlock();
        }
    }

    public void consume(ReadWriteLock readWriteLock) {
        try {
            readWriteLock.writeLock().lock();
            System.out.println("WriteLock acquired by : " + Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (Exception e) {} 
        finally {
            System.out.println("WriteLock releasing by : " + Thread.currentThread().getName());
            readWriteLock.writeLock().unlock();
        }
    }
}
  • MainClass.java
public class MainClass {
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    SharedResourceReadWrite resource1 = new SharedResourceReadWrite();
    SharedResourceReadWrite resource2 = new SharedResourceReadWrite();

    Thread t1 = new Thread(() -> {
        resource1.produce(readWriteLock);
    });

    Thread t2 = new Thread(() -> {
        resource1.produce(readWriteLock);
    });

    Thread t3 = new Thread(() -> {
        resource2.consume(readWriteLock);
    });

    t3.start();
    t1.start();
    t2.start();
}
  • output
WriteLock acquired by : Thread-2
WriteLock releasing by : Thread-2

ReadLock acquired by : Thread-0
ReadLock acquired by : Thread-1

ReadLock releasing by : Thread-0
ReadLock releasing by : Thread-1

3). Stamped Lock

  • Stamped lock were introduced in java 8. It supports Read and Write locks but there is a unique feature called optimistic reads.

What are Optimistic Reads

  • Instead of acquiring full read lock (which blocks writers), it allows a thread to read the resource without locking it, for better performance.

Key Points:

  1. No Blocking of writers:

    • When a thread performs an optimistic read using tryOptmisticRead() , it does not acquire a lock that blocks other threads (including writers).

    • A writer thread can acquire writer lock and modify the shared resource at the same time as the optimistic read.

  2. Potential Data Inconsistency:

    • Since the optimistic read does not block writers, there’s a possibility that the data being read become inconsistent if a writer modifies it during the optimistic read.
  3. Validation:

    • After the optimistic read, the reading threads must validate its optimistic read stamp using validate(stamp).

    • If the validation fails (i.e, a writer modified the resource during the optimistic read), the reader falls back to acquiring a proper read lock to ensure consistent data.

validate(long stamp):

  • It compares the current lock state with the state when the optimistic read stamp was generated.

  • If the lock state is unchanged, the method returns true, meaning the data read is consistent.

  • if the lock state has changed, the method returns false, indicating the data might be inconsistent, and you need to retry or acquire a proper lock.