Quiz yourself: Create worker threads using Runnable and Callable (advanced)

Test your knowledge of what happens when you use the ExecutorService.

October 12, 2020 | Download a PDF of this article
More quiz questions available here

If you have worked on our quiz questions in the past, you know none of them is easy. They model the difficult questions from certification examinations. The “intermediate” and “advanced” designations refer to the exams rather than to the questions, although in almost all cases, “advanced” questions will be harder. We write questions for the certification exams, and we intend that the same rules apply: Take words at their face value and trust that the questions are not intended to deceive you but to straightforwardly test your knowledge of the ins and outs of the language.

The objective of this Java SE 11 quiz is to create worker threads using Runnable and Callable, and then use an ExecutorService to execute tasks concurrently.

Given the following two classes:


class Task1 implements Callable {
	@Override
	public String call() throws Exception {
		return "Task1";
	}
}

class Task2 implements Runnable {
	@Override
	public void run() {
	}
	
	@Override
	public String toString() {
		return "Task2";
	}		
}

and the following code fragment from a method:


Task1 t1 = new Task1();
Task2 t2 = new Task2();
ExecutorService es = Executors.newSingleThreadExecutor();
var f1 = es.submit(t1).get();
var f2 = es.submit(t2).get(); // line n1
System.out.print(f1 + " " + f2);

What will be the result of running the method? Choose one.

A. Task1 Task2
B. Task1 null
C. Runtime exception at line n1
D. Compilation failure due to line n1
 

Answer. With the first releases of Java, any task that was to be performed in a new thread would be encapsulated in an instance of the Runnable interface. However, the run method of a Runnable has a void return type and cannot throw any checked exceptions. Java 5 removed those restrictions with the introduction of the Callable interface. Along with this, the ExecutorService was added as a convenient way of executing tasks presented as either Callable or Runnable objects.

The fundamental difference between the java.util.concurrent.Callable and java.lang.Runnable interfaces is that Callable encapsulates a task in a call method that’s expected to return a value and is declared as throwing Exception. In contrast, a Runnable contains a task that does not return a result and is limited to throwing unchecked exceptions. To support returning data, the Callable interface is generic, so a Callable<E> declares a call method that returns generic type E, like this:


public interface Callable<E> {
  E call() throws Exception;
}

Although the code given for this quiz does not provide a type parameter for the Task1 implementation of Callable, it will issue a warning not an error, unless it is explicitly configured to do so with the -Xlint:all compiler directive.

Of course, a Runnable can provide output data in other ways, just not by means of a return value.

The Executor interface, also added in Java 5, provides for a simple thread pool. Using an Executor instance, you can execute a Runnable in a thread provided from a pool, rather than having to create a new Thread object (which, in current JVMs, typically allocates a new operating system thread) for each such task.

The ExecutorService interface extends the Executor interface and adds both the ability to process Callable tasks (in addition to Runnable tasks) and to interact with the tasks that have been submitted for execution. More specifically, when a task is submitted for execution by an ExecutorService, the service returns a Future object. The Future object acts as a handle on the job, allowing for interaction with that job.

These interactions are limited but very useful. The holder of a Future can

  • Determine whether the task’s execution has been completed
  • Extract the result of a completed task
  • Request cancellation of a task
  • Determine whether a task was canceled

The ExecutorService declares several overloaded versions of submit methods. This quiz uses the following two of them:

  • <T> Future<T=> submit(Callable<T> task);
  • Future<?> submit(Runnable task);

Notice that when you submit a Runnable, the result is presented as a Future<?>. The reality—and a key to this quiz—is that the Runnable.run() returns void, but the get method of a Future must return something.

In Java, void represents the absence of anything and is not an object. Contrast this with other languages that use a specific value to represent the absence of anything, for example undefined in JavaScript, None in Python, and the Unit type represented by the literal () in Scala.

To address this situation, where a Runnable is submitted, the resulting Future’s get method will return null. This might be viewed as imperfect, but Java’s generics mechanism is built around reference types, and currently it does not entirely hide the fact that void returns are handled with structurally different mechanisms from reference returns. Of course, primitive types are also handled differently from reference types, and they also are not supported directly by generics. A major use of the autoboxing mechanism is to minimize the visible consequences of that difference.

How do you know that submit(Runnable task) returns a Future that returns null? The ExecutorService API documentation notes that “The Future’s get method will return null upon successful completion.”

Based on the preceding observations, it should be clear that line n1 does not prevent the code from compiling; thus, option D is incorrect.

It’s also clear that both tasks would complete successfully. One task is just an empty method, and the other simply returns a string.

You also know that both submit methods return Future instances, and both of these Future instances provide a get method. In the case of the submission of a Callable, the get method returns the task result (or throws an exception). In the case of the submission of a Runnable, the get method returns null (or throws an exception).

Because get returns null from the Runnable task’s Future, there is no way for the toString method of that task to be invoked. This tells you that option A must be incorrect.

What will happen when you call the following line?


var f2 = es.submit(t2).get();

You know that get() always returns null. From the var documentation, you know that the following code won’t compile:


var f2 = null; // fails, cannot infer type of f2

However, the compiler makes determinations based on the compile-time type not the runtime value. So, the Future instance’s type parameter is used to determine the type of f2. In this case, it’s an unbounded wildcard (?), which will make var f2 of type Object. Therefore, you know null can be assigned to it successfully.

From this, you can conclude that the code prints Task1 null. Therefore, option C is incorrect.

Conclusion: The correct answer is option B.

Simon Roberts

Simon Roberts joined Sun Microsystems in time to teach Sun’s first Java classes in the UK. He created the Sun Certified Java Programmer and Sun Certified Java Developer exams. He wrote several Java certification guides and is currently a freelance educator who publishes recorded and live video training through Pearson InformIT (available direct and through the O’Reilly Safari Books Online service). He remains involved with Oracle’s Java certification projects.

Mikalai Zaikin

Mikalai Zaikin is a lead Java developer at IBA IT Park in Minsk, Belarus. During his career, he has helped Oracle with development of Java certification exams, and he has been a technical reviewer of several Java certification books, including three editions of the famous Sun Certified Programmer for Java study guides by Kathy Sierra and Bert Bates.

Share this Page