Briefly explain the Future programming pattern of Java

  • 2020-04-01 04:21:19
  • OfStack

Those of you who have used Java and sent packages may be familiar with Future (interface). In fact, Future itself is a widely used concurrency design pattern, which can greatly simplify the development of concurrent applications requiring data flow synchronization. In some domain languages, such as Alice ML, Future is even supported directly at the syntax level.

Here in Java. Util. Concurrent. The Future, for example simple said that the Future way of the specific work. The Future object itself can be thought of as an explicit reference, a reference to the result of the asynchronous processing. Because of its asynchronous nature, the objects it references may not be available at the time of creation (for example, in operation, in transit, or waiting). In this case, the program flow of Future can do whatever it wants if it is not in a hurry to use the object referenced by Future. When the process goes to the object referenced behind Future, there may be two situations:

Hopefully, you'll see the object available and complete some of the subsequent processes. If it's really not available, you can go into another branch process.
"My life would be meaningless without you, so even if the seas run dry and the rocks crumble, I will wait for you." (of course, it's understandable to set a timeout if you don't have the willpower to wait)
In the former case, you can determine whether the referenced object is ready by calling future.isdone () and do it differently. In the latter case, you simply call get() or
Get (long timeout, TimeUnit unit) waits for the object to be ready by synchronously blocking. Whether the actual runtime blocks or returns immediately depends on when the get() is called and when the object is ready.

In simple terms, the Future pattern can meet the data-driven concurrency requirements in continuous processes, which not only achieves the performance improvement of concurrent execution, but also achieves the simplicity and elegance of continuous processes.

Contrast with other concurrent design patterns
In addition to Future, other common concurrency design patterns include callback-driven (in multi-threaded environments), message-driven/event-driven (in the Actor model), and so on.

Callbacks are the most common asynchronous concurrency pattern, which has advantages such as high immediacy and simple interface design. But compared with Future, its disadvantages are also very obvious. First, callbacks in multithreaded environments are typically executed in the module thread that triggered the callback, which means that thread mutex must usually be considered when writing callback methods. Second, it is relatively unsafe for the provider of the callback mode interface to execute the callback applied by the user in the thread of this module, because you cannot determine how long it will take or what exceptions will occur, which may indirectly affect the immediacy and reliability of this module. Furthermore, using the callback interface is detrimental to sequential process development because the execution of the callback method is isolated and it is difficult to merge with the normal process. So the callback interface is suitable for scenarios where simple tasks need to be done in the callback and other processes do not have to be merged.

The shortcomings of these callback patterns are precisely the strengths of Future. Since the use of futures naturally incorporates asynchronous data-driven flows into sequential flows, you don't have to worry about thread exclusion at all, and futures can even be implemented in single-threaded programming models (such as coroutine) (see "Lazy Future" below). On the other hand, a module that provides a Future interface does not have to worry at all about reliability issues like the callback interface and possible immediate impact on the module.

Another common type of concurrency design pattern is "message (event) driven", which is typically used in the Actor model: the service requester sends a message to the service provider, then continues with subsequent tasks that do not depend on the result of the service processing, terminates the current process and records the state before the dependent result is needed; After waiting for the response message, the subsequent process is triggered based on the state of the record. This kind of state machine-based concurrency control is more suitable for sequential processes with continuity than callbacks, but the developer has to cut the continuous process into several state-differentiated sub-processes according to the invocation of asynchronous services, thus artificially increasing the complexity of development. Using the Future pattern avoids this problem by breaking a continuous process for asynchronous invocation. But it's important to note that the future.get () method can block the execution of threads, so it usually doesn't fit directly into the normal Actor model. (the Actor model based on the coroutine can better solve this conflict)

Future's flexibility is also reflected in its free choice between synchronous and asynchronous, with developers free to decide whether to wait [future.isdone ()], when to wait [future.get ()], and how long to wait [future.get ()] depending on the needs of the process. For example, you can decide whether to use this gap to complete other tasks based on whether the data is ready or not, which is quite convenient for implementing the "asynchronous branch prediction" mechanism.

The Future of
In addition to the basic forms mentioned above, the Future has a wealth of derivative changes. Here are a few common ones.

Lazy Future
Unlike a typical Future, Lazy Future does not actively start preparing the referenced object when it is created, but waits until the object is requested to do so. Therefore, Lazy Future itself is not to achieve concurrency, but to save unnecessary computing resources as a starting point, the effect is similar to Lambda/Closure. For example, when designing an API, you might need to return a set of information, some of which can be computationally expensive. But callers don't necessarily care about all of this information, so providing resource-intensive objects in the form of Lazy Future can save resources when callers don't need specific information.

In addition, Lazy Future can also be used to avoid unnecessary mutual exclusion caused by early acquisition or locking of resources.

Promise
Promise can be regarded as a special branch of Future. The common Future is usually the asynchronous processing process directly triggered by the service caller, for example, the processing is triggered immediately when the service is invoked or the processing is triggered when the Lazy Future is used. But Promise is used to explicitly represent situations where the asynchronous process is not directly triggered by the service caller. For example, the timing control of the Future interface, whose asynchronous process is triggered not by the caller but by the system clock, or the Future subscription interface provided by taobao's distributed subscription framework, whose availability of waiting data is not determined by the subscriber, but by when the publisher publishes or updates the data. Therefore, compared with the standard Future, the Promise interface generally has an extra set() or fulfill() interface.

Reusable Future
Regular futures are disposable, meaning that when you get the asynchronous result, the Future object itself becomes meaningless. But a specially designed Future can also be reused, which is useful for data that can be changed many times. For example, the aforementioned Future interface provided by the taobao distributed subscription framework allows multiple calls to the waitNext () method (equivalent to future.get ()), with each call blocking depending on whether there is another data release after the last call, or blocking until the next data release if there is no update. The advantage of this design is that the user of the interface can respond to changes in the subscription data in an infinite loop in a separate thread at any appropriate time, while taking care of other timing tasks and even waiting for multiple futures. Examples of simplification are as follows:


for (;;) {
 schedule = getNextScheduledTaskTime();
 while(schedule > now()) {
  try {
   data = subscription.waitNext(schedule - now());
   processData(data);
  } catch(Exception e) {...}
 }
 doScheduledTask();
}

The use of the Future
This is an example of Callable used in concurrency. The code is as follows:


//: concurrency/CallableDemo.java
import java.util.concurrent.*;
import java.util.*;

class TaskWithResult implements Callable<String> {
 private int id;
 public TaskWithResult(int id) {
  this.id = id;
 }
 public String call() {
  return "result of TaskWithResult " + id;
 }
}

public class CallableDemo {
 public static void main(String[] args) {
  ExecutorService exec = Executors.newCachedThreadPool();
  ArrayList<Future<String>> results =
   new ArrayList<Future<String>>();
  for(int i = 0; i < 10; i++)
   results.add(exec.submit(new TaskWithResult(i)));
  for(Future<String> fs : results)
   try {
    // get() blocks until completion:
    System.out.println(fs.get());
   } catch(InterruptedException e) {
    System.out.println(e);
    return;
   } catch(ExecutionException e) {
    System.out.println(e);
   } finally {
    exec.shutdown();
   }
 }
} //:~ 

To explain how Future is used, the ExecutorService object exec calls the submit () method to produce a Future object, parameterized with a specific type of result returned by Callable. You can use the isDone() method to find out if the Future is complete. When the task is complete, it has a result, and you can call the get() method to get the result. You can also call get() without checking isDone(), in which case get() will block until the result is ready. You can call the get() function with a timeout, or you can call isDone() to see if the task is complete, and then you can call get().


Related articles: