2

My multi-threaded class is supposed to carry out three operations – operation1, operation2, and operation3 – on a number of objects of the class ClassA, where each type of operation is dependant on the earlier operation. For this, I have tried to implement the producer-consumer pattern using a number of BlockingQueues and an ExecutorService.

final ExecutorService executor = ForkJoinPool.commonPool();
final BlockingQueue<ClassA> operationOneQueue = new ArrayBlockingQueue<>(NO_OF_CLASS_A_OBJECTS);
final BlockingQueue<ClassA> operationTwoQueue = new ArrayBlockingQueue<>(NO_OF_CLASS_A_OBJECTS);
final BlockingQueue<ClassA> operationThreeQueue = new ArrayBlockingQueue<>(NO_OF_CLASS_A_OBJECTS);
final BlockingQueue<ClassA> resultQueue = new ArrayBlockingQueue<>(NO_OF_CLASS_A_OBJECTS);

The operations are implemented like this:

void doOperationOne() throws InterruptedException {
    ClassA objectA = operationOneQueue.take();
    objectA.operationOne();
    operationTwoQueue.put(objectA);
}

where each type of operation has its own corresponding method, with its "own" in-queue and out-queue. Each operation method calls the appropriate method on the ClassA object. The method doOperationThree puts ClassA objects in the resultQueue, meaning they have been completely processed.

First, I fill the operationOneQueue with all ClassA objects that are to be operated on. Then, I try to assign executable tasks to the ExecutorService like this:

    while (resultQueue.size() < NO_OF_CLASS_A_OBJECTS) {
        executor.execute(() -> {
            try {
                doOperationOne();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        executor.execute(() -> {
            try {
                doOperationTwo();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        executor.execute(() -> {
            try {
                doOperationThree();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
    executor.shutdown();

Running my program, I get a java.util.concurrent.RejectedExecutionException.

    Operation1: ClassA object 0
    Operation2: ClassA object 0
    Operation1: ClassA object 1
    Operation3: ClassA object 0
    ....
    Operation1: ClassA object 46
    Operation2: ClassA object 45
    Operation3: ClassA object 45
    Exception in thread "main" java.util.concurrent.RejectedExecutionException: Queue capacity exceeded
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.growArray(ForkJoinPool.java:912)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.lockedPush(ForkJoinPool.java:867)
at java.base/java.util.concurrent.ForkJoinPool.externalPush(ForkJoinPool.java:1911)
at java.base/java.util.concurrent.ForkJoinPool.externalSubmit(ForkJoinPool.java:1930)
at java.base/java.util.concurrent.ForkJoinPool.execute(ForkJoinPool.java:2462)
at concurrent.operations.Program1.main(Program1.java:96)

What am I doing wrong? How can I achieve this without over-saturating the thread pool?

Edit: Full disclosure – this is homework with some requirements. 1. I must use ForkJoinPool.commonPool() and must not set the number of threads myself, 2. I must use the consumer-producer pattern, and 3. I must not modify ClassA.

  • Why are you using `ForkJoinPool.commonPool()`? Try `Executors. fixedThreadPool()`. – daniu Feb 17 '20 at 18:53
  • This is home work. Restraints include I must use ForkJoinPool.commonPool() and not set the number of threads myself. – hi_tech_jazz Feb 17 '20 at 20:21
  • In that case you might want to put up the full constraints. What you're doing doesn't look wrong, it's just that submitting tasks overtakes them finishing at some point. If I were given the constraints you've given up to now, I'd use `CompletableFuture`s which are perfectly suited and run in that common pool IIUC. – daniu Feb 17 '20 at 20:46
  • 1
    I just tried your code locally, it seems to finish successfully. Are you sure the number of `ClassA` instances you create is equal to the value of `NO_OF_CLASS_A_OBJECTS`? Double check your `doOperationX` methods as well, whether they `take` from and `put` in the right queues. – sp00m Feb 18 '20 at 10:35
  • Thanks, @sp00m, I have double-checked both those things. When I have more than about 40 `ClassA` instances, on my computer, the program breaks. – hi_tech_jazz Feb 18 '20 at 15:40
  • @daniu, thanks for the suggestions, I have edited the post to include all the constraints. – hi_tech_jazz Feb 18 '20 at 15:40
  • I'm also wary about the "consumer-producer pattern". This is usually data that is being worked on, rather than the actual processing being "consumed". It does work in this context, but it is somewhat weird for sure. It also doesn't say whether you need to have the three operations running in separate threads; I don't see the point in that, and since your requirements don't say so, I wouldn't. – daniu Feb 20 '20 at 09:37

1 Answers1

0

I really like doing concurrent stuff, so I did try writing it. I did use CompletableFuture which a) does run in the ForkJoinPool.commonPool by default and b) makes the actual processing really easy:

while (true) {
    final ClassA nextOperation = queue.take();
    CompletableFuture.runAsync(nextOperation::operationOne)
        .thenRun(nextOperation::operationTwo)
        .thenRun(nextOperation::operationThree)
        .thenRun(() -> resultQueue.add(nextOperation));
}

This will take ClassA objects from the queue and execute all their operations concurrently, but in order.

You did leave out where the tasks are coming from, and whether you need the consumer to terminate. Generally you don't want to, and it does make matters a bit more complicated.

private static final int COUNT = 10;
private static final Random RANDOM = new Random();

public static void main(String[] args) throws ExecutionException, InterruptedException {
    BlockingQueue<ClassA> runnables = new ArrayBlockingQueue<>(COUNT);
    BlockingQueue<ClassA> finished = new ArrayBlockingQueue<>(COUNT);

    // start producer
    ExecutorService createTaskExecutor = Executors.newSingleThreadExecutor();
    createTaskExecutor.submit(() -> fillQueue(runnables));
    // wait for all consumer tasks to finish
    while (finished.size() != COUNT) {
        try {
            // we need to poll instead of waiting forever
            // because the last tasks might still be running
            // while there are no others to add anymore
            // so we need to check again if all have finished in the meantime
            final ClassA nextOperation = runnables.poll(2, TimeUnit.SECONDS);
            if (nextOperation != null) {
                CompletableFuture.runAsync(nextOperation::operationOne)
                        .thenRun(nextOperation::operationTwo)
                        .thenRun(nextOperation::operationThree)
                        .thenRun(() -> finished.add(nextOperation));
            }
        } catch (InterruptedException e) {
            System.err.println("exception while retrieving next operation");
            // we will actually need to terminate now, or probably never will
            throw e;
        }
    }
    System.out.printf("finished tasks (%d):%n", finished.size());
    for (ClassA classA : finished) {
        System.out.printf("finished task %d%n", classA.designator);
    }
    createTaskExecutor.shutdown();
}

private static void fillQueue(BlockingQueue<ClassA> runnables) {
    // start thread filling the queue at random
    for (int i = 0; i < COUNT; i++) {
        runnables.add(new ClassA(i));
        try {
            Thread.sleep(RANDOM.nextInt(1_000));
        } catch (InterruptedException e) {
            System.err.println("failed to add runnable");
        }
    }
}

Since you didn't provide ClassA, I used this one. It contains an identifier so you can track which is running at what time.

class ClassA {
    private static final Random RANDOM = new Random();
    public final int designator;

    public ClassA(int i) {
        designator = i;
    }

    public void operationOne() {
        System.out.printf("%d: operation 1%n", designator);
        sleep();
    }

    public void operationTwo() {
        System.out.printf("%d: operation 2%n", designator);
        sleep();
    }

    public void operationThree() {
        System.out.printf("%d: operation 3%n", designator);
        sleep();
    }

    private static void sleep() {
        try {
            Thread.sleep(RANDOM.nextInt(5_000));
        } catch (InterruptedException e) {
            System.err.println("interrupted while executing task");
        }
    }
}
daniu
  • 14,137
  • 4
  • 32
  • 53