5 Java Thread Communication Techniques: From volatile to LockSupport

This article explains five Java thread communication methods—volatile flag, Object wait/notify, CountDownLatch, ReentrantLock with Condition, and LockSupport—with runnable code examples showing how each coordinates a producer thread adding items and a consumer thread triggered at the fifth addition.

Architect's Guide
Architect's Guide
Architect's Guide
5 Java Thread Communication Techniques: From volatile to LockSupport

Problem Statement

Two threads: Thread A adds "abc" strings to a collection ten times. When the fifth element is added, Thread B should be notified and execute its business logic. Thread communication models are shared memory and message passing; all five methods implement these models.

1. Using volatile Keyword

Based on shared memory: multiple threads monitor a volatile boolean variable. When the variable changes, threads perceive it and act. This is the simplest approach.

public class TestSync { static volatile boolean notice = false; public static void main(String[] args) { List<String> list = new ArrayList<>(); Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("Thread A adds element, list size: " + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) notice = true; } }); Thread threadB = new Thread(() -> { while (true) { if (notice) { System.out.println("Thread B receives notification, starts business..."); break; } } }); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }

2. Using Object wait()/notify()

Object provides wait(), notify(), notifyAll() for thread communication. Must be used within synchronized blocks. wait() releases the lock; notify() does not release the lock—it only tells waiting threads they can compete for the lock again.

public class TestSync { public static void main(String[] args) { Object lock = new Object(); List<String> list = new ArrayList<>(); Thread threadA = new Thread(() -> { synchronized (lock) { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("Thread A adds element, list size: " + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) lock.notify(); } } }); Thread threadB = new Thread(() -> { while (true) { synchronized (lock) { if (list.size() != 5) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread B receives notification, starts business..."); } } }); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }

Output shows that after Thread A calls notify(), Thread B only runs after Thread A finishes its loop, confirming notify() does not release the lock while wait() does.

3. Using JUC CountDownLatch

Since JDK 1.5, java.util.concurrent provides CountDownLatch based on AQS, maintaining a shared state variable. It simplifies coordination: Thread A counts down when condition met; Thread B awaits.

public class TestSync { public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(1); List<String> list = new ArrayList<>(); Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("Thread A adds element, list size: " + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) countDownLatch.countDown(); } }); Thread threadB = new Thread(() -> { while (true) { if (list.size() != 5) { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread B receives notification, starts business..."); break; } }); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }

4. Using ReentrantLock with Condition

ReentrantLock's Condition provides similar wait/signal semantics. Condition.await() releases the lock; Condition.signal() does not. Code is more complex and suffers same issue: Thread B cannot run immediately after signal because Thread A still holds the lock.

public class TestSync { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); List<String> list = new ArrayList<>(); Thread threadA = new Thread(() -> { lock.lock(); for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("Thread A adds element, list size: " + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) condition.signal(); } lock.unlock(); }); Thread threadB = new Thread(() -> { lock.lock(); if (list.size() != 5) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread B receives notification, starts business..."); lock.unlock(); }); threadB.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }

This method behaves like Object wait/notify: the signaling thread retains the lock until it explicitly unlocks.

5. Using LockSupport park/unpark

LockSupport is a flexible blocking/waking tool. It does not require the waiting thread to start first, but you must know the target thread reference. park() blocks the current thread; unpark(Thread) makes the given thread runnable.

public class TestSync { public static void main(String[] args) { List<String> list = new ArrayList<>(); final Thread threadB = new Thread(() -> { if (list.size() != 5) { LockSupport.park(); } System.out.println("Thread B receives notification, starts business..."); }); Thread threadA = new Thread(() -> { for (int i = 1; i <= 10; i++) { list.add("abc"); System.out.println("Thread A adds element, list size: " + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (list.size() == 5) LockSupport.unpark(threadB); } }); threadA.start(); threadB.start(); } }
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyMultithreadingvolatileReentrantLockLockSupportCountDownLatchwait-notify
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.