✅ Ways to Share Data Between Threads in Java
Let’s go step by step from simple to advanced.
🔸 1. Shared Object Reference (Most Common Way)
You create a shared object and pass it to multiple threads.
class Counter {
int count = 0;
}
Counter sharedCounter = new Counter();
Thread t1 = new Thread(() -> {
sharedCounter.count++;
});
Thread t2 = new Thread(() -> {
sharedCounter.count++;
});
❗ Problem: This is not thread-safe. Use synchronized
, AtomicInteger
, or Lock
to fix it.
🔒 How to Make It Safe
✅ Option 1: synchronized
block
class Counter {
int count = 0;
synchronized void increment() {
count++;
}
}
✅ Option 2: AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
AtomicInteger count = new AtomicInteger();
void increment() {
count.incrementAndGet();
}
}
🔸 2. Using BlockingQueue
Perfect for producer-consumer problems.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
Thread producer = new Thread(() -> {
try {
queue.put("Message from producer");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
String message = queue.take();
System.out.println("Consumer received: " + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
✅ Thread-safe and blocking — no need for manual locking.
🔸 3. Using volatile
for visibility
Use volatile
when one thread updates a shared flag or value, and others just read it.
volatile boolean isRunning = true;
Ensures changes are immediately visible to all threads, but does not guarantee atomicity for compound actions (like count++
).
🧠 Summary Table
Method | Thread-safe? | Best for |
---|---|---|
Shared object reference | ❌ (use sync) | Simple shared state |
synchronized | ✅ | Critical sections, low-level control |
AtomicInteger , etc. | ✅ | Lightweight atomic counters |
BlockingQueue | ✅ | Producer-consumer, task queues |
volatile | ✅ (for reads/writes) | Flags, one-thread-writes scenarios |
Java does provide a kind of pipe between threads — and it’s literally called a pipe, through the PipedInputStream
/ PipedOutputStream
and PipedReader
/ PipedWriter
classes!
Let’s explore this idea 💡
🧵🔗 Pipes Between Threads in Java
✅ What is it?
A pipe is a communication channel:
One thread writes data → the other reads it.
It mimics a Unix pipe or producer-consumer model.
📦 Java Pipe Classes:
For Binary Data | For Character Data |
---|---|
PipedInputStream | PipedReader |
PipedOutputStream | PipedWriter |
✅ Example: PipedOutputStream
and PipedInputStream
import java.io.*;
public class PipeDemo {
public static void main(String[] args) throws IOException {
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out);
Thread writer = new Thread(() -> {
try {
out.write("Hello from writer!".getBytes());
out.close();
} catch (IOException e) {
e.printStackTrace();
}
});
Thread reader = new Thread(() -> {
try {
int data;
while ((data = in.read()) != -1) {
System.out.print((char) data);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
});
writer.start();
reader.start();
}
}
🧠 Output:
Hello from writer!
⚠️ Caveats
- Pipes are blocking: reading blocks until data is available.
- Only for communication between two threads — not suitable for multi-thread broadcast or high throughput.
- Not as robust or flexible as
BlockingQueue
.
✅ When to Use
- When you need stream-like communication between two threads.
- For custom producer-consumer logic where you want control over data format (bytes/characters).