6

I have a situation of a single producer and single consumer working with a queue of objects. There are two situations when the queue might be empty:

  1. The consumer handled the objects quicker than the producer was capable of generating new objects (producer uses I/O before generating objects).
  2. The producer is done generating objects.

If the queue is empty, I want the consumer to wait until a new object is available or until the producer signals that it is done.

My research so far got me no where because I still ended up with a loop that checks both the queue and a separate boolean flag (isDone). Given that there's no way of waiting on multiple locks (thought of waiting on the queue AND the flag), what can be done to solve this?

Yon
  • 1,023
  • 4
  • 13
  • 23
  • I've added a proposed solution below if anyone cares to review it. Was it correct to add it as an answer? – Yon Mar 03 '12 at 12:48

5 Answers5

2

First of all, the suggestion that using a wrapper is "too much overhead" is a guess, and IMO a very bad one. This assumption should be measured with a performance test with actual requirements. If and only if the test fails, then verify using a profiler that wrapping the queue object is why.

Still if you do that and wrapping the queue object (in this case a String) really is the cause of unacceptable performance, then you can use this technique: create a known, unique string to serve as an "end of messages" message.

    public static final String NO_MORE_MESSAGES = UUID.randomUUID().toString();

Then when retrieving Strings from the queue, just check (it can be an reference check) if the String is NO_MORE_MESSAGES. If so, then you're done processing.

Sean Reilly
  • 21,526
  • 4
  • 48
  • 62
  • I've responded to @esaj about this too. Say we have a sufficiently unique String, wouldn't the additional overhead of running "equals" on all Strings (thousands per queue, hundreds of queues a minute) be a waste? – Yon Mar 03 '12 at 12:02
  • 1
    Note the answer -- since it's a static final field, you can do a reference equality check (==, not .equals()) for the special string. – Sean Reilly Mar 03 '12 at 15:06
1

Simple. Define a special object that the producer can send to signal "done".

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • In most cases the queue is of Strings. We prefer not to wrap the String object because it will generate an additional overhead. – Yon Mar 03 '12 at 07:53
  • Then use a string value that cannot occur in your normal data. How about a zero-length string? – Jim Garrison Mar 03 '12 at 09:27
1

One option is to wrap your data in a holder object, which can be used to signal the end of processing.

For example:

public class QueueMessage {
 public MessageType type;
 public Object thingToWorkOn;
}

where MessageType is an enum defining a "work" message or a "shutdown" message.

Yann Ramin
  • 32,895
  • 3
  • 59
  • 82
  • This is quite an overhead, especially if its a queue of thousands of Strings, wouldn't you think? – Yon Mar 03 '12 at 07:51
1

You could use LinkedBlockingQueues poll(long timeout, TimeUnit unit) -method in the consumer, and if it returns null (the timout elapsed), check the boolean flag. Another way would be passing some special "EndOfWork"-object into the queue as the last one, so the consumer knows that it's the end of work.

Yet another way would be interrupting the consumer thread from the producer thread, but this would require the producer thread to be aware of the consumer. If they both would be implemented as nested classes, you could use the parent class to hold a boolean running-value, which both could access, and terminate both threads with single boolean.

esaj
  • 15,875
  • 5
  • 38
  • 52
  • The poll with the timer is something I thought of but still seemed quite wasteful. The use of a special object is problematic here because the queue is normally of Strings and we prefer not to wrap them (causes additional overhead). – Yon Mar 03 '12 at 07:55
  • Could you use some special String to denote the end of work and have the producer write it as last String in queue? Something short that will never come up in the normal work-Strings, then just use equals() to see if the work-String is exactly that, and if so, terminate the consumer. – esaj Mar 03 '12 at 08:15
  • The content of the Strings is highly dynamic. If we wanted to create such a string it would be probably quite long and using non-standard characters, which will put a CPU load when running equals so many times. – Yon Mar 03 '12 at 08:25
1

The following option has been raised too (not sure if this should be in an answer to myself but couldn't find a better place to write this):

Create a wrapper for the queue. This wrapper will have a monitor that will be waited on when reading by the consumer and will be notified by the producer whenever either a new object is added or the flag of isDone is raised.

When the consumer reads objects from the queue, these objects will be wrapped with something similar to what @yann-ramin suggested above. To reduce overhead though, the consumer will provide a single, reusable, instance of QueueMessage upon every read call (it will always be the same instance). The queue wrapper will update the fields accordingly before returning the instance to the consumer.

This avoids any use of timeouts, sleeps, etc.

EDITED This is a proposed implementation:

/**
 * This work queue is designed to be used by ONE producer and ONE consumer
 * (no more, no less of neither). The work queue has certain added features, such
 * as the ability to signal that the workload generation is done and nothing will be
 * added to the queue.
 *
 * @param <E>
 */
public class DefiniteWorkQueue<E> {
    private final E[] EMPTY_E_ARRAY;
    private LinkedBlockingQueue<E> underlyingQueue = new LinkedBlockingQueue<E>();
    private boolean isDone = false;

    // This monitor allows for flagging when a change was done.
    private Object changeMonitor = new Object();

    public DefiniteWorkQueue(Class<E> clazz) {
        // Reuse this instance, makes calling toArray easier
        EMPTY_E_ARRAY = (E[]) Array.newInstance(clazz, 0);
    }

    public boolean isDone() {
        return isDone;
    }

    public void setIsDone() {
        synchronized (changeMonitor) {
            isDone = true;
            changeMonitor.notifyAll();
        }
    }

    public int size() {
        return underlyingQueue.size();
    }

    public boolean isEmpty() {
        return underlyingQueue.isEmpty();
    }

    public boolean contains(E o) {
        return underlyingQueue.contains(o);
    }

    public Iterator<E> iterator() {
        return underlyingQueue.iterator();
    }

    public E[] toArray() {
        // The array we create is too small on purpose, the underlying
        // queue will extend it as needed under a lock
        return underlyingQueue.toArray(EMPTY_E_ARRAY);
    }

    public boolean add(E o) {
        boolean retval;
        synchronized (changeMonitor) {
            retval = underlyingQueue.add(o);
            if (retval)
                changeMonitor.notifyAll();
        }
        return retval;
    }

    public boolean addAll(Collection<? extends E> c) {
        boolean retval;
        synchronized (changeMonitor) {
            retval = underlyingQueue.addAll(c);
            if (retval)
                changeMonitor.notifyAll();
        }
        return retval;
    }

    public void remove(RemovalResponse<E> responseWrapper) throws InterruptedException {
        synchronized (changeMonitor) {
            // If there's nothing in the queue but it has not
            // ended yet, wait for someone to add something.
            if (isEmpty() && !isDone())
                changeMonitor.wait();

            // When we get here, we've been notified or
            // the current underlying queue's state is already something
            // we can respond about.
            if (!isEmpty()) {
                responseWrapper.type = ResponseType.ITEM;
                responseWrapper.item = underlyingQueue.remove();
            } else if (isDone()) {
                responseWrapper.type = ResponseType.IS_DONE;
                responseWrapper.item = null;
            } else {
                // This should not happen
                throw new IllegalStateException(
                    "Unexpected state where a notification of change was     made but " +
                        "nothing is in the queue and work is not     done.");
            }
        }
    }

    public static class RemovalResponse<E> {
        public enum ResponseType {
            /**
             * Used when the response contains the first item of the queue.
             */
            ITEM,

            /**
             * Used when the work load is done and nothing new will arrive.
             */
            IS_DONE
        };

        private ResponseType type;
        private E item;

        public ResponseType getType() {
            return type;
        }

        public void setType(ResponseType type) {
            this.type = type;
        }

        public E getItem() {
            return item;
        }

        public void setItem(E item) {
            this.item = item;
        }

    }
}
Yon
  • 1,023
  • 4
  • 13
  • 23