Java-Threads

 


Check the story, The Shepherd of Threadville : To learn Java multithreading in a funny way 😃

What Is a Thread?
  • A thread is a lightweight sub-process that runs independently within a program.

  • Java supports multithreading to allow concurrent execution.

  • Daemon threads run in the background (e.g., garbage collection). They don’t prevent the JVM from exiting.

Ways to Create Threads in Java
Using Thread Class:
 class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
new MyThread().start();
 Implementing Runnable:
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running");
}
}
new Thread(new MyRunnable()).start();
Using Callable and Future:
Callable<String> task = () -> "Callable result";
Future<String> future = Executors.newSingleThreadExecutor().submit(task);
System.out.println(future.get());

Thread States

  • NEW

  • RUNNABLE

  • BLOCKED

  • WAITING

  • TIMED_WAITING

  • TERMINATED


Wait/Notify Mechanism
Used for communication between threads (especially for producer-consumer)
synchronized(obj) {
obj.wait(); // Waits
obj.notify(); // Wakes up one thread
obj.notifyAll();// Wakes up all waiting threads
}
ConcurrentModificationException & Fail-Fast vs Fail-Safe
Happens when a collection is modified while iterating.
  • Fail-Fast (e.g., ArrayList) throws exception.
  • Fail-Safe (e.g., CopyOnWriteArrayList) allows modification without error by working on cloned data.
Volatile, Synchronized, and Atomic Variables
  • volatile: ensures visibility across threads but doesn’t guarantee atomicity.
  • synchronized: locks access to code blocks or methods.
    •     Object-level locksynchronized(this)
    •     Class-level locksynchronized(MyClass.class)
  • Atomic classes (e.g., AtomicInteger) provide lock-free thread safety.
Making Collections Thread-Safe

Use Collections.synchronizedList() or thread-safe classes like:

  • ConcurrentHashMap

  • CopyOnWriteArrayList

  • BlockingQueue

Locks in Java
ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}

Other Locks:

  • ReadWriteLock

  • StampedLock

  • ReentrantReadWriteLock

ThreadLocal and InheritableThreadLocal
  • ThreadLocal: provides isolated variables per thread.

  • InheritableThreadLocal: child threads inherit values from parent.


ExecutorService & Thread Pools
Thread PoolDescription
newFixedThreadPool(n)Reuses a fixed number of threads
newCachedThreadPool()        Creates new threads as needed
newSingleThreadExecutor()Executes tasks sequentially
newScheduledThreadPool(n)Executes tasks after delay

Example:
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> System.out.println("Running task"));
pool.shutdown();

Producer-Consumer Problem
Traditional Way Using wait and notify:
class SharedQueue {
Queue<Integer> queue = new LinkedList<>();
// synchronized methods with wait/notify
}
Using BlockingQueue:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
queue.put(1); // producer
queue.take(); // consumer
CompletableFuture
CompletableFuture is part of the java.util.concurrent package. It represents a future result of an asynchronous computation and allows you to chain multiple tasks together.
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Running async task");
});
future.join(); // Wait for completion
With Result:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Hello from CompletableFuture!";
});
System.out.println(future.join()); // Prints result
Chaining Tasks:
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Step 1")
.thenApply(step -> step + " -> Step 2")
.thenApply(step -> step + " -> Step 3");

System.out.println(future.join());
// Output: Step 1 -> Step 2 -> Step 3
Handling Exceptions:
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
if (true) throw new RuntimeException("Oops!");
return "Success";
})
.exceptionally(ex -> "Handled error: " + ex.getMessage());

System.out.println(future.join());
Combining Multiple Futures:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<String> combined = future1.thenCombine(future2, (a, b) -> a + " " + b);
System.out.println(combined.join()); // Output: Hello World
CountDownLatch
Used to delay thread execution until other threads have completed.
CountDownLatch latch = new CountDownLatch(3);

Runnable task = () -> {
System.out.println("Task done");
latch.countDown(); // Reduces the count
};

new Thread(task).start();
new Thread(task).start();
new Thread(task).start();

latch.await(); // Waits until count reaches zero
System.out.println("All tasks completed");
CyclicBarrier
Synchronizes threads to wait for each other at a common barrier point.
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads reached the barrier");
});

Runnable task = () -> {
System.out.println("Task reached barrier");
try {
barrier.await(); // Waits for others
} catch (Exception e) {
e.printStackTrace();
}
};

new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
Java Memory Model (JMM)
  • Defines how threads interact through memory.

  • Ensures visibility and ordering of shared variables.

  • Addresses issues like instruction reordering or stale values.

Thread Dumps and Analysis
Thread dumps are vital for diagnosing issues like deadlocks, performance bottlenecks, or stuck threads.
Take a Thread Dump:
  • On Unix/Linux/macOS: kill -3 <pid>

  • On Windows (using jstack): jstack <pid> > threadDump.txt

  • Inside IDEs: Many Java IDEs like IntelliJ and Eclipse offer built-in thread dump tools.

Analyze a Thread Dump:
  • Look for threads stuck in BLOCKED or WAITING state.
  • Check stack traces for nested locks or unending loops.
  • Identify thread names, thread IDs, priority levels, and locks held.
  • Use visualization tools like:
    • FastThread.io
    • JMC (Java Mission Control)
    • IntelliJ’s diagnostic tools
Virtual Threads:

Virtual threads are lightweight threads managed by the Java Virtual Machine (JVM) rather than the operating system. Unlike traditional platform threads, they don’t tie up OS resources when blocked (e.g., waiting for I/O). This means you can spawn millions of virtual threads without overwhelming the system.

  • They’re ideal for tasks that spend most of their time waiting—like database queries, HTTP calls, or file I/O.

  • They don’t speed up execution but allow greater concurrency with simpler code.

How to Create Virtual Threads:

Using Thread.ofVirtual()
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("Running in a virtual thread!");
});
thread.join(); // Wait for completion
Using Executors.newVirtualThreadPerTaskExecutor()
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
System.out.println("Hello from virtual thread pool!");
});
executor.shutdown();
Using Thread.Builder API
ThreadFactory factory = Thread.builder().virtual().factory();
Thread thread = factory.newThread(() -> {
System.out.println("Built with Thread.Builder");
});
thread.start();
When Should we Use Virtual Threads?

Use virtual threads when:

  • You have high-throughput applications (e.g., web servers) handling thousands of concurrent tasks.

  • Your tasks are I/O-bound, like calling APIs, reading files, or querying databases.

  • You want to avoid the complexity of asynchronous programming (e.g., CompletableFuture, reactive streams).

Avoid virtual threads for:

  • CPU-intensive tasks (e.g., image processing, data crunching).

  • Code that heavily uses synchronized blocks or native methods, which can pin the underlying carrier thread.


Programming Tasks:
  1. Create a Thread That Prints Numbers from 1 to 100

    • Use both Thread and Runnable implementations.

  2. Implement a Simple Counter with Multiple Threads

    • Launch 5 threads that increment a shared counter. Ensure correct synchronization.

  3. Producer-Consumer Problem Using wait() and notify()

    • Use a shared Queue and synchronized methods.

  4. Solve Producer-Consumer Using BlockingQueue

    • Implement producers and consumers with proper thread coordination.

  5. Print Even and Odd Numbers with Two Threads

    • One thread prints even numbers, another prints odd. Use synchronization to alternate correctly.

  6. Create a Task Execution Using ExecutorService

    • Submit tasks and collect results using Callable and Future.

  7. Implement a ThreadPool That Processes File Names in Parallel

    • Given a list of file names, read contents concurrently and print the first line from each.

  8. Use CompletableFuture to Run Multiple Tasks in Parallel

    • Combine results from two services and log a final output.

  9. Demonstrate ThreadLocal Usage

    • Simulate an API call that stores user-specific context in a thread-local variable.

  10. Simulate Bank Transactions from Multiple Threads

  • Ensure account balance updates are thread-safe using synchronized or ReentrantLock.

  1. Build a Countdown Timer with CountDownLatch

  • Use 3 threads to perform different tasks and wait for all to finish before proceeding.

  1. Synchronize Data Writing and Reading Using CyclicBarrier

  • Synchronize multiple writer threads that trigger a reader once all complete.

  1. Handle Shared Access Using Atomic Variables

  • Replace synchronized block with AtomicInteger for a shared counter.

  1. Compare Performance Between synchronized, ReentrantLock, and StampedLock

  • Benchmark with 1000 threads incrementing a counter.

  1. Simulate a Deadlock

  • Purposefully create a deadlock and then analyze with thread dumps.

Also Read: