1 Message passing with threads
Strategy: Use a synchronized queue for message passing between threads. Java provides the BlockingQueue
interface for queues with blocking operations:
In an ordinary Queue
:
- add(e) adds element e to the end of the queue.
- remove() removes and returns the element at the head of the queue, or throws an exception if the queue is empty.
A BlockingQueue
extends this interface:
additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.
put(e)
blocks until it can add element e to the end of the queue (if the queue does not have a size bound, put will not block).take()
blocks until it can remove and return the element at the head of the queue, waiting until the queue is non-empty.
notice: use put()
andtake()
, not use add()
andremove()
.
1.1 producer-consumer design pattern
- Producer threads and consumer threads share a synchronized queue.
- Producers put data or requests onto the queue, and consumers remove and process them.
- One or more producers and one or more consumers might all be adding and removing items from the same queue.
Java provides two implementations of BlockingQueue
:
ArrayBlockingQueue
is a fixed-size queue that uses an array representation. put ting a new item on the queue will block if the queue is full.LinkedBlockingQueue
is a growable queue using a linked-list representation. If no maximum capacity is specified, the queue will never fill up, so put will never block.
2 Implementing message passing with queues
- The message in the queue must be an immutable type.
- we must design our messages here to prevent race conditions and enable clients to perform the atomic operations they need. For example,
withdraw-if-sufficient-funds
would be a better operation than justwithdraw
.
3 Stopping
- One strategy is a poison pill: a special message on the queue that signals the consumer of that message to end its work.
- It is also possible to interrupt a thread by calling its
interrupt()
method.
- If the thread is blocked waiting, the method it’s blocked in will throw an InterruptedException (that’s why we have to try-catch that exception almost any time we call a blocking method).
- If the thread was not blocked, an interrupted flag will be set. The thread must check for this flag to see whether it should stop working.
for example:
public void run() {
// handle requests until we are interrupted
while ( ! Thread.interrupted()) {
try {
// block until a request arrives
int x = in.take();
// compute the answer and send it back
int y = x * x;
out.put(new SquareResult(x, y));
} catch (InterruptedException ie) {
// stop
break;
}
}
}
4 Thread safety arguments with message passing
- Existing threadsafe data types for the synchronized queue. This queue is definitely shared and definitely mutable, so we must ensure it is safe for concurrency.
- Immutability of messages or data that might be accessible to multiple threads at the same time.
- Confinement of data to individual producer/consumer threads. Local variables used by one producer or consumer are not visible to other threads, which only communicate with one another using messages in the queue.
- Confinement of mutable messages or data that are sent over the queue but will only be accessible to one thread at a time. This argument must be carefully articulated and implemented. But if one module drops all references to some mutable data like a hot potato as soon as it puts them onto a queue to be delivered to another thread, only one thread will have access to those data at a time, precluding concurrent access.
Reference
[1] 6.005 — Software Construction on MIT OpenCourseWare | OCW 6.005 Homepage at https://ocw.mit.edu/ans7870/6/6.005/s16/