When we pass a runnabble to an executorService like
Future future = executorService.submit(runnable);
// As here executorService maps the object into instance of
new FutureTask(runnable); and after that it executes the runnable using addWorker(runnable) -> which internally uses thread for execution.
As talking about callable
Future future = executorService.submit(callable); // As here executorService maps the object into instance of
new FutureTask(callable);
How does JVM maps the callable and make it run concurrently. How does it uses the Thread to execute callable and maps the return result of the callable method in FutureTask instance.
because Thread has a very tight contract with run() method, How does JVM manages to call the call() method of callable if it uses a Thread to execute the callable.
CodePudding user response:
Yes, the Callable
gets executed by whichever thread grabs the task.
I think you're giving Runnable
too much importance. The Thread
class does implement Runnable
, but that is not what makes the code multithreaded. That comes from Java starting an OS-level thread when you call the Thread#start()
method (ignoring virtual threads). After starting the thread, that new thread then calls the Thread#run()
method. Any code that gets executed as a consequence of that run()
method being executed is executed on said new thread (ignoring communication between threads).
By default, the Thread#run()
method simply calls the Runnable#run()
method on the Runnable
instance given to the Thread
when it was instantiated.
Now, you seem to be talking about ThreadPoolExecutor
. Note that is only one implementation of ExecutorService
, but I will focus on it, too. Internally, ThreadPoolExecutor
creates one or more Thread
instances and starts them when and as appropriate. These Thread
s do not execute your submitted Runnable
or Callable
implementations directly. Instead, each Thread
is created with an internal implementation of Runnable
: Worker
.
Vastly simplified, this worker essentially does the following:
while (!shutdown) {
Runnable task = taskQueue.take();
task.run();
}
Where taskQueue
is an instance of BlockingQueue<Runnable>
. When you submit a Runnable
or Callable
, they get put in this queue. This is how tasks are submitted by one thread but executed by another.
However, the Runnable
or Callable
you submit is not put in the queue directly. First it wraps your object in another that understands how to communicate a result back. In other words, an implementation of Future
. But this Future
still needs to also be a Runnable
, which is where the RunnableFuture
interface comes from. The implementation of this interface that is used by ThreadPoolExecutor
is FutureTask
, at least by default (you can override certain methods to return your own implementation). So, by default, it is a FutureTask
that wraps your object that is actually put into the queue.
The FutureTask
class internally wraps any Runnable
in a Callable
. Again simplified, it essentially does:
// just the constructors
public FutureTask(Callable<V> callable) {
this.callable = callable;
}
public FutureTask(Runnable r, V result) {
this.callable = () -> {
r.run();
return result;
};
}
Note: If you do executorService.submit(runnable)
then result
will ultimately be null
.
Then the FutureTask#run()
method (as seen here) executes the call()
method of the Callable
. Once again vastly simplified, it essentially does:
@Override
public void run() {
try {
this.result = this.callable.call();
} catch (Exception ex) {
this.error = ex;
}
}
Note: This ignores the relatively complex code needed to synchronize the state of the task between threads, so that e.g., calls to get()
wait for the result as needed (whether a return value or an exception). If you're interested in that, take a look at the implementation linked above.
Given the FutureTask#run()
method is executed by whatever thread grabbed it from the task queue, the Callable
will be executed by said thread.