This article explains the use of the Future pattern in Java. The article is also posted on the public account (click to view).
1.1 What is the Future model
For example, when we write a function, the statements in the function are executed line by line synchronously. If a line is slow, the program must wait until the end of the execution before returning the result. But sometimes we may not need the execution result of one of these lines so badly that we want the called person to return immediately. For example, Xiaoming has successfully created an account on a website, and an email notification will be sent after the creation of the account. If the email notification takes a long time for some reason (at this time, the account has been successfully created), the result of successful creation will be returned to the front end after the traditional synchronous execution. But at the moment we don’t need to immediately after account creation success has concerned mail sent successfully, now you can use the Future pattern, let Ann slowly to deal with the request in the background, for the caller can then deal with other tasks that need data in real situations (such as some want to know whether the email success) to try to obtain the data needed.
With the Future pattern, data may not be immediately available when fetched. Instead, you get a wrapper and you can go to GET and get the data you need when you need it.
1.2 Differences between Future model and traditional model
Looking at the sequence diagram of the request return, it is clear that the traditional pattern is sequential synchronous execution, with only waiting when time consuming operations are encountered. In Future mode, when a time-consuming operation is initiated, the function returns immediately without blocking the client thread. So the client doesn’t have to wait while the actual time consuming operation is performed. It can do other things until it needs to get the result from the worker thread.
2.1. Implement simple Future mode
The DataFuture class below is just a wrapper class that can be created without blocking and waiting. The setRealData method is used to pass the data in after the worker thread has it ready. The client simply calls the getRealData method when it really needs data and returns immediately if the data is ready, otherwise the getRealData method waits until the data is obtained.
public class DataFuture<T> {
private T realData;
private boolean isOK = false;
public synchronized T getRealData(a) {
while(! isOK) {try {
// Wait until data is ready
wait();
} catch(Exception e) { e.printStackTrace(); }}return realData;
}
public synchronized void setRealData(T data) {
isOK = true; realData = data; notifyAll(); }}Copy the code
When the client requests data to the server, the server will not immediately load the real data, just create a DataFuture, create child threads to load the real data, the server directly return DataFuture.
public class Server {
public DataFuture<String> getData(a) {
final DataFuture<String> data = new DataFuture<>();
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run(a) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setRealData("Final data"); }});returndata; }}Copy the code
The final client call code is as follows:
long start = System.currentTimeMillis();
Server server = new Server();
DataFuture<String> dataFuture = server.getData();
try {
// Perform other operations first
Thread.sleep(5000);
// Simulation time...
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("Result data:" + dataFuture.getRealData());
System.out.println("Time consuming:" + (System.currentTimeMillis() - start));
Copy the code
Results:
Result data: Final data time:5021
Copy the code
The final data takes about 5 seconds to execute, or about 10 seconds if executed sequentially.
Future and FutureTask in JDK
Take a look at the Future interface source:
public interface Future<V> {
/** * is used to cancel a task. Return true on success or false on failure. * The mayInterruptIfRunning parameter indicates whether a task that is executing but has not completed can be canceled. If it is set to true, a task that is executing can be canceled. * If the task is complete, this method returns false whether mayInterruptIfRunning is true or false, i.e., false if the completed task is canceled; If the task is executing, return true if mayInterruptIfRunning is set to true, or false if mayInterruptIfRunning is set to false; * If the task has not yet been executed, it must return true, whether mayInterruptIfRunning is true or false. * /
boolean cancel(boolean mayInterruptIfRunning);
/** * indicates whether the task was cancelled successfully. If the task was cancelled successfully before it completed normally, return true */
boolean isCancelled(a);
/** * indicates whether the task has been completed, and if so returns true */
boolean isDone(a);
/** * get the result of the execution. If the final result has not been obtained, the method blocks until the result is returned */
V get(a) throws InterruptedException, ExecutionException;
/** * get the result of the execution. If the result is not obtained within the specified time, throw a TimeoutException */
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
Copy the code
As you can see from the above source code, the Future is to query the execution of a Runnable or Callable task, interrupt the task, and get the result. The following is an example of calculating the sum of 100 million to 100 million to see how much time difference is between using the traditional way and using the Future. First look at the traditional mode code:
public class FutureTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
List<Integer> retList = new ArrayList<>();
// Calculate the sum of 100 to 100 million 1000 times
for (int i = 0; i < 1000; i++) {
retList.add(Calc.cal(100000000));
}
System.out.println("Time consuming:" + (System.currentTimeMillis() - start));
for (int i = 0; i < 1000; i++) {
try {
Integer result = retList.get(i);
System.out.println("The first" + i + "A result:" + result);
} catch (Exception e) {
}
}
System.out.println("Time consuming:" + (System.currentTimeMillis() - start));
}
public static class Calc implements Callable<Integer> {
@Override
public Integer call(a) throws Exception {
return cal(10000);
}
public static int cal (int num) {
int sum = 0;
for (int i = 0; i < num; i++) {
sum += i;
}
returnsum; }}}Copy the code
Execution result (40+ seconds) :
Time:43659
第0Results:887459712
第1Results:887459712
第2Results:887459712. The first999Results:887459712Time:43688
Copy the code
Let’s take a look at the application using Future mode:
public class FutureTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<Integer>> futureList = new ArrayList<>();
// Calculate the sum of 100 to 100 million 1000 times
for (int i = 0; i < 1000; i++) {
// Schedule execution
futureList.add(executorService.submit(new Calc()));
}
System.out.println("Time consuming:" + (System.currentTimeMillis() - start));
for (int i = 0; i < 1000; i++) {
try {
Integer result = futureList.get(i).get();
System.out.println("The first" + i + "A result:" + result);
} catch (InterruptedException | ExecutionException e) {
}
}
System.out.println("Time consuming:" + (System.currentTimeMillis() - start));
}
public static class Calc implements Callable<Integer> {
@Override
public Integer call(a) throws Exception {
return cal(100000000);
}
public static int cal (int num) {
int sum = 0;
for (int i = 0; i < num; i++) {
sum += i;
}
returnsum; }}}Copy the code
Execution result (12+ seconds) :
Time:12058
第0Results:887459712
第1Results:887459712. The first999Results:887459712Time:12405
Copy the code
As you can see, the time for concurrent execution of 1000 sums between 100 and 100 million using Future mode is about 30 seconds faster than using traditional methods. The efficiency of using Future mode is greatly improved.
2.3, FutureTask
With a Future, because a Future is an interface and cannot be used to create objects directly, we have the following FutureTask. First look at the implementation of FutureTask:
public class FutureTask<V> implements RunnableFuture<V>
Copy the code
You can see that the FutureTask class implements the RunnableFuture interface. Then look at the RunnableFuture interface source code:
public interface RunnableFuture<V> extends Runnable.Future<V> {
/** * Sets this Future to the result of its computation * unless it has been cancelled. */
void run(a);
}
Copy the code
As you can see, the RunnableFuture interface inherits from the Runnable and Future interfaces, which means that the FutureTask can be executed by the thread either as a Runnable or as a Future to get a Callable return value.
Looking at the two constructors of FutureTask below, you can see that they are intended for these two operations.
public FutureTask(Callable<V> var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
this.callable = var1;
this.state = 0; }}public FutureTask(Runnable var1, V var2) {
this.callable = Executors.callable(var1, var2);
this.state = 0;
}
Copy the code
FutureTask Example:
public class FutureTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Calc task = new Calc();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
executor.shutdown();
}
public static class Calc implements Callable<Integer> {
@Override
public Integer call(a) throws Exception {
return cal(100000000);
}
public static int cal (int num) {
int sum = 0;
for (int i = 0; i < num; i++) {
sum += i;
}
returnsum; }}}Copy the code
2.4 shortcomings of the Future
As you can see from the example above, using the Future pattern is much more efficient than the traditional pattern. To some extent, using the Future pattern allows tasks in a thread pool to execute asynchronously. However, there is an obvious drawback: callbacks cannot be executed on a different thread from the task. The biggest problem with traditional callbacks is that they cannot separate the control flow between different event handlers. For example, the main thread needs to wait for the results of the asynchronous execution threads to return, so it must block in the future.get() method and wait for the results to return. This is synchronization again, and it is even worse if one thread takes too long to execute.
In Java8, a new implementation class, CompletableFuture, was introduced to make up for this shortcoming, and the use of CompletableFuture will be explained in the next section.