package org.threadly.concurrent;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import org.threadly.util.AbstractService;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.ExceptionUtils;
/**
* Producer consumer problems are very frequent within multi-threaded code. This class is
* designed to be a throttle for both sides of the problem. It takes in a {@link BlockingQueue}
* so that items are only consumed as they become available. At the same time it has a
* {@link ConsumerAcceptor} that will only accept items as it is ready. By accepting on the same
* thread as the consumer it will only try to take more items after the
* {@link ConsumerAcceptor#acceptConsumedItem(Object)} call returns.
* <p>
* Another way to look at it, this class provides the thread to handle blocking when consuming
* from a {@link BlockingQueue}.
* <p>
* Keep in mind that this class in no way attempts to solve the problem if the program is
* producing faster than the consumer accepts. In those conditions the queue will still continue
* to grow, and consume memory.
*
* @since 1.0.0
* @param <T> Type of items contained in the queue to be consumed
*/
public class BlockingQueueConsumer<T> extends AbstractService {
private static final AtomicInteger DEFAULT_CONSUMER_VALUE = new AtomicInteger(0);
private static String getDefaultThreadName() {
return "QueueConsumer-" + DEFAULT_CONSUMER_VALUE.getAndIncrement();
}
protected final ThreadFactory threadFactory;
protected final String threadName;
protected final BlockingQueue<? extends T> queue;
protected final ConsumerAcceptor<? super T> acceptor;
protected volatile Thread runningThread;
/**
* Constructs a new consumer, with a provided queue to consume from, and an acceptor to accept
* items.
*
* @param threadFactory ThreadFactory to construct new thread for consumer to run on
* @param queue queue to consume from
* @param acceptor acceptor to provide consumed items to
*/
public BlockingQueueConsumer(ThreadFactory threadFactory,
BlockingQueue<? extends T> queue,
ConsumerAcceptor<? super T> acceptor) {
this(threadFactory, null, queue, acceptor);
}
/**
* Constructs a new consumer, with a provided queue to consume from, and an acceptor to accept
* items.
*
* @param threadFactory ThreadFactory to construct new thread for consumer to run on
* @param threadName Name of thread consumer runs on, or {@code null} to generate a default one
* @param queue queue to consume from
* @param acceptor acceptor to provide consumed items to
*/
public BlockingQueueConsumer(ThreadFactory threadFactory,
String threadName,
BlockingQueue<? extends T> queue,
ConsumerAcceptor<? super T> acceptor) {
ArgumentVerifier.assertNotNull(threadFactory, "threadFactory");
ArgumentVerifier.assertNotNull(queue, "queue");
ArgumentVerifier.assertNotNull(acceptor, "acceptor");
this.threadFactory = threadFactory;
this.threadName = threadName;
this.queue = queue;
this.acceptor = acceptor;
runningThread = null;
}
@Override
protected void startupService() {
runningThread = threadFactory.newThread(new ConsumerRunnable());
if (runningThread.isAlive()) {
throw new IllegalThreadStateException();
}
runningThread.setDaemon(true);
if (threadName == null || threadName.isEmpty()) {
runningThread.setName(getDefaultThreadName());
} else {
runningThread.setName(threadName);
}
runningThread.start();
}
@Override
protected void shutdownService() {
Thread runningThread = this.runningThread;
this.runningThread = null;
runningThread.interrupt();
}
/**
* This function is provided so that it can be Overridden if necessary. One example would be if
* any locking needs to happen while consuming.
*
* @return the next consumed item
* @throws InterruptedException thrown if thread is interrupted while blocking for next item
*/
protected T getNext() throws InterruptedException {
return queue.take();
}
/**
* Class which represents our runnable actions for the consumer.
*
* @since 1.0.0
*/
private class ConsumerRunnable implements Runnable {
@Override
public void run() {
while (runningThread != null) {
try {
T next = getNext();
acceptor.acceptConsumedItem(next);
} catch (InterruptedException e) {
stopIfRunning();
// reset interrupted status
Thread.currentThread().interrupt();
} catch (Throwable t) {
ExceptionUtils.handleException(t);
}
}
}
}
/**
* Interface for an implementation which can accept consumed tasks. You must provide an
* implementation of this interface on construction of the {@link BlockingQueueConsumer}.
*
* @since 1.0.0
* @param <T> Type of item this acceptor will receive
*/
public interface ConsumerAcceptor<T> {
/**
* Called when ever the queue consumer has removed an item from the queue. This call should
* block until the acceptor is ready for another item.
*
* @param item Object that was removed from the queue
* @throws Exception possible exception that could be thrown
*/
public void acceptConsumedItem(T item) throws Exception;
}
}