If I was looking at SynchronousQueue without the above context, I would have wondered why would anyone need a queue that's not really a queue but more like a pointer swap between appropriate threads. But the use-case I'm dealing with ("event bursts") are probably the perfect use case for preventing the consumers from overwhelming rates by modeling the problem more as a "hand-off" than a typical case of buffered queuing. The central idea behind this data-structure is to adapt queue idioms without using a queue in a very efficient manner with an added feature that message production is rate limited by consumer's speed of processing them. Behind the scenes, it uses dual-stack/queue algorithm (depending on ordering fairness preference) to transfer a reference between threads.
SynchronousQueue is more of a thread queue than a data queue, it maintains a stack/queue of waiter threads (i.e. "consumers") and not the queue of data itself. You can probably achieve the same functionality by using BlockingQueue of size 1 or using an explicit object lock and explicit wait/notify on a datum reference like an example below:
//Example code, probably riddled with concurrency bugs
//(I've only tested it on my laptop :))
public class MyNaiveSyncQueue { private final Object LOCK = new Object(); private volatile Object data; //volatile is needed for non compressed OOPS public void put(Object o) throws InterruptedException{ synchronized (LOCK) { if(data != null){ LOCK.wait(); } data = o; LOCK.notify(); } } public Object take() throws InterruptedException{ synchronized (LOCK) { if(data == null){ LOCK.wait(); } Object o = data; data = null; LOCK.notify(); return o; } } }
There are several problems with the solution above:
- Violent locking and memory fence overhead: for individual queue operations, this will scale terribly with number of producers/consumers, especially on server class SMP hardware.
- Constant context switching: each successful queue operation involves syscall(s) for context switching which might involve kernel scheduler and everything that comes with it (cache flush/register reload et. al.).
- Overhead for fair processing: JVM manages object monitor wait queues in a fixed FIFO order, there's certain overhead in seeking the first enqueued thread to schedule as a consumer. This may or may not be the behavior the programmer cares about.
So far this has been working great for the system I'm dealing with which processes about a couple of hundred messages per millisecond at peak bursts but I realize that it might not be appropriate (or even worth it) for non-realtime/non-bursty producers.
No comments:
Post a Comment