Method of creating a thread in Java

  • Extend the thread class Override the run() method
    • Con: You cannot extend any other class
  • Implement runnable and pass it to the constructor of the thread class (recommended)
    • Pros: more flexible and we can extend other classes
    • Cons: run() method cannot return a result or throw checked exceptions
      • The run function is a void and cannot return some value or throw exceptions which can lead to race conditions or silent failures
      • The solution is callable and future > return type is Generics

Keywords

  • volatile keyword is used to tell a thread that a variable should not be cached and can be changed by any other thread
    • ensure memory visibility but not atomicity
  • synchronized keyword ensures that only one thread can execute a specific block of code at a time, preventing thread interference
    • NOT to be used when:
      • high concurrency scaling
      • read-heavy workloads > use ReentrantReadWriteLock instead
      • simple increments > use AtomicInteger instead
      • if you need to lock two objects and release them in a specific order > use ReentrantLock
    • Common types of keys for synchronized
      • this - current instance - the object that is calling the function
      • private dedicated lock - create specific objects to act as keys for specific pieces of data
      • ClassName.class - a global key that locks the entire class for every instance
      • external locking - synchronize two different classes based on a shared resource
  • wait and notify keywords

Thread Pools

Executor Framework - Managing Threads

CountDownLatch

1. Summary: What is CountDownLatch?

A CountDownLatch is a concurrency utility that allows one or more threads to wait until a set of operations performed in other threads completes. It works like a “one-way gate”:

  • It is initialized with a count.
  • The await() method blocks until the count reaches zero.
  • The countDown() method decrements the counter.
  • Once the count reaches zero, the gate opens and cannot be reset.

2. Practical Use Cases

  • Service Startup Dependency: Ensuring a main application server only starts after its required modules (e.g., Database, Cache, Messaging Queue) have signaled they are ready.
  • Parallel Test Execution: Waiting for multiple parallel test cases to finish before generating a final aggregate report.
  • The “Starting Gun”: Initializing several threads and having them all wait on a latch with a count of 1. When the main thread calls countDown(), all threads start simultaneously.

3. Brief Code Example

This example demonstrates a Main Task waiting for three Worker Tasks to finish. Java

import java.util.concurrent.CountDownLatch;
public class LatchExample {
    public static void main(String[] args) throws InterruptedException {
        int workerCount = 3;
        CountDownLatch latch = new CountDownLatch(workerCount);
        for (int i = 1; i <= workerCount; i++) {
            new Thread(new Worker(i, latch)).start();
        }
        System.out.println("Main thread is waiting for workers...");
        latch.await(); // Blocks until count is 0
        System.out.println("All workers finished. Main thread proceeding.");
    }
}
class Worker implements Runnable {
    private final int id;
    private final CountDownLatch latch;
    Worker(int id, CountDownLatch latch) { this.id = id; this.latch = latch; }
    public void run() {
        System.out.println("Worker " + id + " is doing work.");
        latch.countDown(); // Decrements the count
    }
}

4. When to Use vs. When Not to Use

Use it when:

  • You have a one-time synchronization point.
  • You need one thread to wait for different events (even if those events happen on the same thread).
  • The number of operations is known at the time of initialization.

Do NOT use it when:

  • You need to reuse the latch: If you need the counter to reset after it reaches zero (e.g., in a recurring algorithm), use a CyclicBarrier.
  • Dynamic parties: If the number of threads/tasks changes during execution, a Phaser is more flexible.
  • Complex state sharing: If you need to pass data between threads as they wait, consider a CompletableFuture or Exchanger. TODO:
  • Callable and Future
  • Read about Generics
  • Virtual Threads in Java 21+
  • Dealing with shared data (Synchronization)
    • synchronized
    • Reentrant Lock
    • Atomic
    • Concurrent Collections