Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
89: Producer-Consumer with BlockingQueue
When I first started working with multi-threaded Java, I fell into a trap that I see almost every junior developer hit: I thought that a BlockingQueue was just a "thread-safe list" and that I still needed to write my own synchronized blocks and wait()/notify() logic around it to make the Producer and Consumer actually talk to each other.
The Myth: You Need Manual Synchronization Around the Queue
The misconception is that the BlockingQueue only prevents the internal data structure from being corrupted. People think, "Sure, the queue won't crash if two threads touch it, but I still need to tell the Consumer to wake up when the Producer adds something."
If you do this, your code ends up looking like a mess of nested blocks:
// DON'T DO THIS
synchronized(queue) {
while(queue.isEmpty()) {
queue.wait();
}
T item = queue.take();
}
This is completely redundant. By wrapping a BlockingQueue in a synchronized block, you're essentially putting a lock on a lock. You're adding massive overhead and increasing the risk of deadlocks for absolutely no gain. The "Blocking" part of BlockingQueue is the most important word in the name—it means the coordination is already baked into the methods.
The Reality: BlockingQueue Is a Coordination Tool, Not Just a List
The magic happens in the put() and take() methods. These aren't just getters and setters; they are synchronization points.
put(E e): If the queue is full, the producer thread doesn't just throw an error or return false. It literally pauses (blocks) right there until a consumer takes something out and creates space.take(): If the queue is empty, the consumer thread doesn't spin in awhile(true)loop wasting CPU cycles. It goes to sleep until the producer puts something in.
I like to think of it as a physical conveyor belt with a limited length. If the belt is full, the person loading it has to stop. If the belt is empty, the person taking things off has to wait. Neither person needs a walkie-talkie to tell the other what to do; the state of the belt tells them everything they need to know.
Let's look at a real-world scenario: a log processing system where one thread reads raw log lines from a file and another thread parses those lines and saves them to a database. We use an ArrayBlockingQueue here to prevent the producer from reading the entire file into memory if the database is slow.
import java.util.concurrent.*;
public class LogProcessor {
public static void main(String[] args) {
// Limit the queue to 100 lines to prevent OutOfMemoryError
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// The Producer: Reads the "file"
Runnable producer = () -> {
try {
for (int i = 1; i <= 10; i++) {
String log = "Log Entry #" + i;
System.out.println("Producing: " + log);
queue.put(log); // Blocks if queue is full
Thread.sleep(100); // Simulate reading time
}
queue.put("POISON_PILL"); // Tell consumer we are done
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
// The Consumer: Writes to "DB"
Runnable consumer = () -> {
try {
while (true) {
String log = queue.take(); // Blocks if queue is empty
if ("POISON_PILL".equals(log)) break;
System.out.println("Consuming: " + log + " -> Saved to DB");
Thread.sleep(200); // Simulate DB latency
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
new Thread(producer).start();
new Thread(consumer).start();
}
}
Notice how clean that is. No wait(), no notifyAll(), and no manual locks. The BlockingQueue handles the signaling internally. I used a "Poison Pill" (the POISON_PILL string) because the consumer's take() method will block forever if the producer finishes and the consumer is still waiting. It's a common pattern to send a special signal to gracefully shut down your consumer threads.
📋 Practical Task
Exercise: Implementing a Concurrent Email Dispatcher
You are building a system that sends notification emails. To avoid overloading the SMTP server, you need to decouple the request to send an email from the actual sending process.
Requirements:
- Create an
EmailTaskclass that holds a recipient email address and a message body. - Implement a
Dispatcherclass that uses aLinkedBlockingQueue(with a capacity of 50) to hold pendingEmailTaskobjects. - Create a
Producerthread that generates 20 randomEmailTaskobjects and puts them into the queue. - Create two
Consumerthreads (Worker threads) thattake()tasks from the queue and print:"Thread [ID] sending email to [email]...". - Ensure the program terminates gracefully. You must use the "Poison Pill" strategy: the producer should put two "special" empty
EmailTaskobjects (one for each consumer) into the queue after it finishes producing the 20 emails.
Goal: Verify that both consumer threads process the emails concurrently and both exit the loop when they encounter the poison pill.
There are no comments for now.