Skip to main content

Multi-Threading

1. Ways to create multi-threads

  • Extends Thread class
      class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println("Hello");
    }
    }

    public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    }
    • Pros:
      1. Simple to write
      2. When needing to access the current thread, you can directly use this without calling Thread.currentThread()
    • Cons:
      1. Since the thread class already inherits from Thread, it cannot inherit from any other parent class
  • Implements Runnable interface
      class MyRunnable implements Runnable {
    @Override
    public void run() {
    System.out.println("MyRunnable");
    }
    }

    public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable());
    t.start();
    }
    • Pros:
      1. The thread class only implements Runnable interface, allowing it to inherit from other classes.
      2. Multiple threads can share the same target object, making it highly suitable for scenarios where multiple threads.need to access the same resource.
    • Cons:
      1. Slightly more complex.
      2. If you need to access the current thread, you must use Thread.currentThread().
      3. Cannot return a result.
  • Implement Callable interface and FutureTask
    • java.util.concurrent.Callable interface is similar to Runnable, but the call() method returns a result and thorw exceptions.
    • To execute Callable task, it needs to be wrapped in a FutureTask object because the constructor of Thread class takes a Runnable object, and FutureTask implements Runnable interface.
      class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    return "Hello World";
    }
    }

    public static void main(String[] args) {
    MyCallable myCallable = new MyCallable();
    FutureTask<String> futureTask = new FutureTask<>(myCallable);
    Thread t = new Thread(futureTask);
    t.start();

    try {
    String res = futureTask.get();
    System.out.println("Result: " + res);
    } catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
    }
    }
    • Pros
      1. Same as Runnable.
    • Cons
      1. Same as Runnable.
  • Extends ExecutorService
    • Starting from Java5, java.util.concurrent.ExecutorService introduced support for thread pools.
    • A way more efficient to manage threads, avoiding the overhead of creating and destroying threads.
    • Different types of thread pools can be created using the static methods of the Executors class.
      class Task implements Runnable {
    @Override
    public void run() {
    System.out.println("Task start");
    }
    }

    ExecutorService executor = Executors.newFixedThreadPool(10);
    for(int i = 0; i < 100; i++) {
    executor.submit(new Task());
    }
    executor.shutdown();
    • Pros
      1. Can resue pre-created threads, avoiding the overhead of thread creation and destruction.
      2. For concurrent requests requiring quick responses, thread pools can rapidly provide threads to handle the tasks, reducing wait time.
      3. Thread pools can control the number of running threads, preventing system resource exhausion due to creating too many threads.
      4. By properly configuring the thread pool size, CPU utilization and system throughput can be optimized.
    • Cons
      1. Increase program complexity, especially in tuning thread pool parameters and troubleshooting issues.
      2. Incorrect configurations may lead to problems such as deadlocks or resource exhaustion.

2. How to prevent deadlocks?

  • A deadlock occurs only when the following four conditions are simultaneously met:

    1. Mutual Exclusion: Multiple threads cannot use the same resource simulatenously.
    2. Hold and Wait: Refers to situation where Thread A, which already holds Resource 1, want to acquire Resource 2. However, Resource 2 is held by Thread B, so Thread A enters a waiting state. While Thread A is waiting for Resource 2, Thread A does not release Resource 1.
    3. No Preemption Condition: Once a thread holds a resource, it cannot be taken away by other threads until the thread releases the resource voluntarily. If Thread B wants to use this Resource 1, it can only acquire it after Thread A has finished using and released it.
    4. Circular Wait Condition: During a deadlock, the order in which two or more threads acquire resources forms a circular chain.
    Deadlock Example
      private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    Thread threadA = new Thread(() -> {
    synchronized (resource1) {
    System.out.println("Thread A acquired resource1");
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource2) {
    System.out.println("Thread A acquired resource2");
    }
    }
    });

    Thread threadB = new Thread(() -> {
    synchronized (resource2) {
    System.out.println("Thread B acquired resource2");
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource1) {
    System.out.println("Thread B acquired resource1");
    }
    }
    });

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

    // Output: Thread 1 acquired resource1
    // Output: Thread 2 acquired resource2
  • Most common approach:

    Resource Ordering Allocation Method

    • To break the circular wait condition.
    • Thread A and Thread B must acquire resources in the same order. When Thread A attempts to acquire Resource A first and then Resource B, Thread B must also attempt to acquire Resource A first and then Resource B.

3. What are the differences and similarities between wait and sleep in Java?

Class

  1. sleep: static method that is defined in the Thread class
  2. wait: instance method that is defined in the Object class

Lock Release Behavior

  1. Thread.sleep(): When called, the thread pauses execution for the specified time but does not release any object locks it holds.
  2. Object.wait(): When called, the thread releases the object lock it holds and enters a waiting state until another thread calls notify() or notifyAll() on the same object to wake it up.

Usage Condition

  1. sleep: Can be called anywhere without the need to acquire a lock beforehand.
  2. wait: must be called within a synchronized block or synchronized method (i.e., the thread must hold the object’s lock); otherwise, it throws an IllegalMonitorStateException.

Wake-up Mechanism

  1. sleep: After the specified sleep duration ends, the thread automatically returns to the runnable state, awaiting CPU scheduling.
  2. wait: The thread remains in the waiting state until another thread calls notify() or notifyAll() on the same object to wake it up. notify() randomly wakes up one thread waiting on the object, while notifyAll() wakes up all threads waiting on the object.

4. What are the statuses of a thread?

  • java.lang.Thread.State enum class defines six thread states. You can call getState() method on a Thread object to get the current state of the thread.

    StateDescription
    NEWThe thread has not yet started, i.e., it has been created but the start method has not been called.
    RUNNABLEThe thread is in the ready state (after calling start, awaiting scheduling) or actively running.
    BLOCKEDThe thread is blocked, waiting to acquire a monitor lock to enter a synchronized block or method.
    WAITINGThe thread is in a waiting state, waiting for another thread to perform a specific action (e.g., Object.notify()).
    TIMED_WAITINGThe thread is in a waiting state with a specified timeout.
    TERMINATEDThe thread has completed execution and is in a terminated state.

5. Describe the lifecycle of a Java thread?

  • The lifecycle of a thread is divided into five states:
    1. New: The thread has just been created using the new method but has not yet started.
    2. Runnable: After calling the start() method, the thread is in a state where it is waiting for CPU resources to be allocated.
    3. Running: When a runnable thread is scheduled and obtains CPU resources, it enters the running state.
    4. Blocked: While in the running state, a thread may enter a blocked state due to certain reasons, such as calling sleep() or wait(). Woken threads do not immediately execute the run method; they must wait again for CPU resource allocation to re-enter the running state.
    5. Terminated: If the thread completes execution normally, is forcibly terminated prematurely, or ends due to an exception, the thread is destroyed, releasing its resources.

6. How do different threads communicate with each other?

  1. Shared variables volatile

    1. Multiple threads can access and modify the same shared variable to exchange information.
    2. To ensure thread safety, it is necessary to use synchronized or volatile keyword.
    Multiple Threads - Volatile
      private static volatile boolean flag = false;

    public static void main(String[] args) {
    Thread producer = new Thread(() -> {
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    flag = true;
    System.out.println("Producer: Flag is set to true.");
    });

    Thread consumer = new Thread(() -> {
    while (!flag) {
    }
    System.out.println("Consumer: Flag is now true.");
    });

    producer.start();
    consumer.start();
    }
  2. wait(), notify(), and notifyAll() methods in Object class

    1. The wait() method causes the current thread to enter a waiting state.
    2. The notify() method wakes up a single thread that is waiting on this object's monitor.
    3. The notifyAll() method wakes up all threads that are waiting on this object's monitor.
      class WaitNotifyExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
    // Thread producer
    Thread producer = new Thread(() -> {
    synchronized (lock) {
    try {
    System.out.println("Producer: Producing...");
    Thread.sleep(2000); // Simulate production time
    System.out.println("Producer: Production finished. Notifying consumer");
    // Notify waiting threads
    lock.notify();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });

    // Thread consumer
    Thread consumer = new Thread(() -> {
    synchronized (lock) {
    try {
    System.out.println("Consumer: Waiting for production to finish...");
    // Wait for notification
    lock.wait();
    System.out.println("Consumer: Production finished. Consuming...");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });

    consumer.start();
    producer.start();
    }
    }
  3. Lock and Condition interfaces

    1. More flexible appraoch compared to synchronized
    2. The await() method of the Condition interface is similar to the wait() method.
    3. The signal() is similar to the notify() method.
    4. The signalAll() is similar to the notifyAll() method.
    5. The ReentrantLock class implements the Lock interface.
      private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) {
    // Producer thread
    Thread producer = new Thread(() -> {
    lock.lock();
    try {
    System.out.println("Producer: Producing...");
    Thread.sleep(2000); // Simulate production time
    System.out.println("Producer: Production finished. Notifying consumer...");
    // Notify waiting threads
    condition.signal();
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    });

    // Consumer thread
    Thread consumer = new Thread(() -> {
    lock.lock();
    try {
    System.out.println("Consumer: Waiting for production to finish...");
    // Wait for notification
    condition.await();
    System.out.println("Consumer: Production finished. Consuming...");
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    });

    consumer.start();
    producer.start();
    }
  4. BlockingQueue Interface

    1. Thread-safe queue operations.
    2. When the queue is full, threads attempting to insert elements will be blocked.
    3. When the queue is empty, threads attempting to retrieve elements will be blocked.
      private static final BlockingQueue queue = new LinkedBlockingQueue<>(1);

    public static void main(String[] args) {

    // Producer thread
    Thread producer = new Thread(() -> {
    try {
    System.out.println("Producer: Producing...");
    queue.put(1); // Blocks if queue is full
    System.out.println("Producer: Production finished.");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    });

    // Consumer thread
    Thread consumer = new Thread(() -> {
    try {
    System.out.println("Consumer: Waiting for production to finish...");
    int item = (int) queue.take(); // Blocks if queue is empty
    System.out.println("Consumer: Consumed item: " + item);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    });

    consumer.start();
    producer.start();
    }

7. What are the methods for inter-thread communication?

8. Describe the differences between sleep() and wait()?

  • sleep(): Thread class, pauses the current thread for a specified time.
  • wait(): Object class, waits for another thread to notify.

9. Describe the differences between notify() and notifyAll()?

  • notify(): Wakes up one thread waiting on the object’s monitor.
  • notifyAll(): Wakes up all threads waiting on the object’s monitor

10. What is the difference between the run() and start() methods of a thread?

Thread Pool

Configuration of Thread Pool.

  1. corePoolSize: The minimum number of threads kept alive in the pool, even when idle.
  2. maximumPoolSize: The maximum number of threads allowed in the pool when the task queue is full.
  3. Queue Capacity: The number of tasks that can be queued when all core threads are busy (before scaling up to maxPoolSize).
  4. KeepAliveTime: The amount of time a non-core thread can remain idle before being terminated.
  5. workQueue: The queue used to store tasks when all core threads are busy.
  6. handler: The policy to use when the task queue is full and all threads are busy.

Built-in Rejection Strategies.

  1. CallerRunsPolicy: The task is executed by the thread that calls the execute method.
  2. AbortPolicy: Directly throws an exception indicating that the task was rejected by the thread pool.
  3. DiscardPolicy: Silently discards the submitted task without any processing.
  4. DiscardOldestPolicy: Discards the oldest task in the queue and then executes the new task.

Do you have experience with thread pool parameter settings?

  • CPU-intensive: corePooSize = coreSize + 1;
    • If the number of threads exceeds the number of CPU core significantly, threads will compete for CPU time, causing frequent context switching.
  • Trade-off of increasing number of threads:
    1. Context Switching
      : CPU switches from one thread to another, cost arise due to save and restore thread's state.

E-commerce:

  • Instantaneous high concurrency
new ThreadPoolExecutor(
16, // corePoolSize = 16 (assuming 8-core CPU × 2)
32, // maximumPoolSize = 32 (expand for burst traffic)
10, TimeUnit.SECONDS, // non-core threads recycled after 10s idle
new SynchronousQueue<>(), // no task caching, directly expand threads
new AbortPolicy() // reject directly to avoid system overload
);

Microservice HTTP Request:

new ThreadPoolExecutor(
16, // corePoolSize = 16 (8-core × 2)
64, // maximumPoolSize = 64 (handle slow downstream)
60, TimeUnit.SECONDS, // non-core threads recycled after 60s idle
new LinkedBlockingQueue<>(200), // bounded queue with capacity 200
new CustomRetryPolicy() // custom rejection policy (retry or fallback)
);
  • CallRunsPolicy():
  • AbortPolicy():