Search code examples
guavaexecutorservice

How to get MoreExecutors.newDirectExecutorService() behavior by using bare ThreadPoolExecutor?


When I run the following code:

package foo.trials;

import com.google.common.util.concurrent.MoreExecutors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class DirectExecutorService {
    private static final Logger logger_ = LoggerFactory.getLogger(DirectExecutoService.class);

    public static void main(String[] args) {
        boolean useGuava = true;

        final ExecutorService directExecutorService;
        if (useGuava) {
            directExecutorService = MoreExecutors.newDirectExecutorService();
        } else {
            directExecutorService = new ThreadPoolExecutor(
                    0, 1, 0, TimeUnit.DAYS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadPoolExecutor.CallerRunsPolicy());
            directExecutorService.submit(new BlockingCallable());
        }

        Future<Boolean> future = directExecutorService.submit(new MyCallable());
        try {
            logger_.info("Result: {}", future.get());
        } catch (InterruptedException e) {
            logger_.error("Unexpected: Interrupted!", e);
        } catch (ExecutionException e) {
            logger_.error("Unexpected: Execution exception!", e);
        }
        logger_.info("Exiting...");
    }

    static class MyCallable implements Callable<Boolean> {
        static final Random _random = new Random();
        @Override
        public Boolean call() throws Exception {
            logger_.info("In call()");
            return _random.nextBoolean();
        }
    }

    static class BlockingCallable implements Callable<Boolean> {

        Semaphore semaphore = new Semaphore(0);
        @Override
        public Boolean call() throws Exception {
            semaphore.acquire(); // this will never succeed.
            return true;
        }
    }
}

I get the following output

13:36:55.960 [main] INFO  a.t.DirectExecutoService - In call()
13:36:55.962 [main] INFO  a.t.DirectExecutoService - Result: true
13:36:55.963 [main] INFO  a.t.DirectExecutoService - Exiting...

Note that all of the execution happens in the main thread. In particular the callable's call get's dispatched in the calling thread. Of course, this is what one would expect from MoreExecutors.newDirectExecutorService() no surprise there.

And I get similar result when I set the variable useGuava to false.

13:45:14.264 [main] INFO  a.t.DirectExecutoService - In call()
13:45:14.267 [main] INFO  a.t.DirectExecutoService - Result: true
13:45:14.268 [main] INFO  a.t.DirectExecutoService - Exiting...

But if I comment out the following line

directExecutorService.submit(new BlockingCallable());

then I get the following output.

13:37:27.355 [pool-1-thread-1] INFO  a.t.DirectExecutoService - In call()
13:37:27.357 [main] INFO  a.t.DirectExecutoService - Result: false
13:37:27.358 [main] INFO  a.t.DirectExecutoService - Exiting...

As one can see the callable's call happens in a different thread pool-1-thread-1. I think I can explain why this happens; perhaps because thread pool can have (upto) 1 thread that's available, so the 1st Callable gets dispatched to that extra thread which otherwise was consumed by BlockingCallable.

My question is how does one create an ExecutorService that'll do what DirectExecutorService does without having to artificially burning a thread with a callable that'll never finish?

Why am I asking this?

  1. I have a codebase that uses guava at version 11.0. I'd need to avoid upgrading it to 17.0+ -- which offers MoreExecutors.newDirectExecutorService() -- if I can.
  2. ThreadPoolExecutor does not allow setting maxThreads to 0. It would be odd if it allowed that but if it did then that'd have also solved my problem.
  3. Lastly, I was surprised to notice this behavior -- I had assumed (mistakenly) that using CallerRunsPolicy would cause all the call of all Callables to be executed in caller's thread. So, I wanted to put my experience and hack out there so someone else could save the hours that ended up burning in trying to understand this. :(

Is there better/more idiomatic way to achieve DirectExecutorService like behavior if one can't upgrade to guava 17.0+?


Solution

  • Is there better/more idiomatic way to achieve DirectExecutorService like behavior if one can't upgrade to guava 17.0+?

    If that's your only issue here, you should use MoreExecutors.sameThreadExecutor(). It's basically newDirectExecutorService() before it was moved to a new method (and directExecutor() was added), see Javadoc:

    Since: 18.0 (present as MoreExecutors.sameThreadExecutor() since 10.0)

    BTW: You should really upgrade to newest Guava, you're using almost six-year-old one!