Friends who have used Java concurrency packages may be familiar with Future (interface). In fact, Future itself is a widely used concurrent design model, which can greatly simplify the development of concurrent applications that require data flow synchronization. Future is even directly supported at the syntax level in some domain languages (such as Alice ML).
Here we will take java.util.concurrent.Future as an example to briefly talk about the specific working method of Future. The Future object itself can be regarded as an explicit reference, a reference to the results of asynchronous processing. Due to its asynchronous nature, the object it references may not be available at the beginning of creation (for example, it is still in operation, network transmission or waiting). At this time, if the program flow that gets Future is not in a hurry to use the object referenced by Future, then it can do anything else you want. When the process goes to the object that needs to be referenced behind Future, there may be two situations:
I hope to see this object available and complete some related follow-up processes. If it is really unavailable, you can also enter other branch processes.
"Without you, my life will lose its meaning, so even if the sea is gone, I will wait for you." (Of course, if you really don't have the perseverance to wait, it's understandable to have an out-of-time)
For the former case, you can determine whether the referenced object is ready by calling Future.isDone() and take different processing; for the latter case, you only need to call get() or
get(long timeout, TimeUnit unit) waits for the object to be ready by synchronous blocking. Whether the actual runtime is blocked or returned immediately depends on the call time of get() and the order of object ready.
Simply put, Future mode can meet data-driven concurrency needs in continuous processes, not only obtaining performance improvements in concurrent execution, but also simplicity and elegance of continuous processes.
Comparison with other concurrent design patterns
In addition to Future, other common concurrency design patterns include "callback driven (in multi-threaded environment)", "message/event driven (in the Actor model)", etc.
Callback is the most common asynchronous concurrency mode, which has high immediacy and simple interface design. But compared to Future, its disadvantages are also very obvious. First, callbacks in multithreaded environments are generally executed in the module thread that triggers the callback, which means that thread mutual exclusion issues must be considered when writing callback methods; secondly, the provider of the callback method interface is in the thread of this module. Execute callbacks for user applications, which are also relatively unsafe, because you cannot determine how long it will take or what exceptions it will occur, which may indirectly affect the immediacy and reliability of this module; furthermore, using the callback interface is not conducive to sequence. Process development, because the execution of callback methods is isolated, it is difficult to merge with normal processes. Therefore, the callback interface is suitable for scenarios where only simple tasks need to be completed in a callback and does not have to be combined with other processes.
The disadvantages of the above callback modes are exactly the strengths of Future. Since the use of Future is to naturally incorporate asynchronous data drivers into sequential processes, you don't have to consider thread mutex issues at all. Future can even be implemented in a single-threaded program model (such as coroutines) (see what will be mentioned below "Lazy Future"). On the other hand, modules that provide Future interfaces do not have to worry about reliability issues like callback interfaces and their potential immediacy impact on this module.
Another common concurrent design pattern is "message (event) driven", which is generally used in the Actor model: the service requester sends a message to the service provider, and then continues to perform subsequent tasks that do not rely on the service processing results, and depends on it if you need to rely on it. The current process is terminated before the result and the status is recorded; the subsequent process is triggered according to the recorded status after waiting for the response message. Although this state machine-based concurrent control is more suitable for continuity sequential processes than callbacks, developers have to cut the continuous process into multiple state-specific subprocesses according to the call of the asynchronous service, which is artificially Increases the complexity of development. Using Future mode can avoid this problem and do not have to break continuous processes for asynchronous calls. However, one thing should be noted: the Future.get() method may block thread execution, so it usually cannot be directly incorporated into the conventional Actor model. (The coroutine-based Actor model can better resolve this conflict)
Future's flexibility is also reflected in its free choice between synchronization and asynchronous choices. Developers can freely decide whether they need to wait [Future.isDone()], when to wait [Future.get()], and how long to wait according to the needs of the process. .get(timeout)]. For example, you can decide whether to use this gap to complete other tasks based on whether the data is ready, which is quite convenient for implementing the "asynchronous branch prediction" mechanism.
The derivative of Future
In addition to the basic forms mentioned above, Future also has rich derivative changes, so here are a few common ones.
Lazy Future
Unlike general Futures, Lazy Future does not actively start preparing the referenced object at the beginning of creation, but waits until the object is requested before starting the corresponding work. Therefore, Lazy Future itself is not intended to achieve concurrency, but takes saving unnecessary computing resources as the starting point, and its effect is similar to Lambda/Closure. For example, when designing certain APIs, you may need to return a set of information, and the calculation of some of them may consume considerable resources. However, the caller may not be concerned about all this information, so providing objects that require more resources in the form of Lazy Future can save resources when the caller does not need to use specific information.
In addition, Lazy Future can also be used to avoid unnecessary mutual exclusions caused by premature acquisition or locking of resources.
Promise
Promise can be regarded as a special branch of Future. Common Future is generally caused by the service caller to directly contact the asynchronous processing process, such as triggering processing immediately when calling the service or triggering processing when the Lazy Future value is taken. But Promise is used to explicitly represent scenarios where asynchronous processes are not directly triggered by the service caller. For example, the timing control of the Future interface, its asynchronous process is not triggered by the caller, but by the system clock. For example, the Future subscription interface provided by Taobao's distributed subscription framework, its waiting data availability is not determined by the subscriber, but by the subscriber. When will the publisher publish or update the data. Therefore, compared with the standard Future, the Promise interface generally has an extra set() or fulfill() interface.
Reusable Future
A regular Future is one-time, which means that when you obtain asynchronous processing results, the Future object itself loses its meaning. But specially designed Future can also be reused, which is very useful for data that can be changed multiple times. For example, the Future-style interface provided by the Taobao distributed subscription framework mentioned above allows multiple calls to waitNext() method (equivalent to Future.get()). Whether it blocks each time it is called depends on whether it is after the last call. There is another data release. If there is no update yet, it will block until the next data release. The advantage of this design is that the user of the interface can respond to changes in subscription data at any suitable time, or simply through an infinite loop in an independent thread, while also taking into account other timing tasks, or even waiting for multiple tasks at the same time. Future. Simplified examples are as follows:
for (;;) { schedule = getNextScheduledTaskTime(); while(schedule > now()) { try { data = subscription.waitNext(schedule - now()); processData( data); } catch(Exception e) {.. .} } doScheduledTask();} Use of Future
First, let’s list a piece of code in Thinking in Java. This is an example of the use of Callable in concurrency. The code is as follows:
//: concurrency/CallableDemo.javaimport 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 ex ec = 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(Ex e) { System. out.println(e); } finally { exec.shutdown(); } }} /* Output:result of TaskWithResult 0result of TaskWithResult 1result of TaskWithResult 2res ult of TaskWithResult 3result of TaskWithResult 4result of TaskWithResult 5result of TaskWithResult 6result of TaskWithResult 7result of TaskWithResult 8result of TaskWithResult 9*///:~To explain the use of Future, first, the ExecutorService object exec calls submit() method to generate a Future object, which uses the specific type of the callable return result to parameterize. You can use the isDone() method to query whether the Future has been completed. When the task is completed, it has a result, and you can call the get() method to get the result. You can also call get() directly without isDone() checking. In this case, get() will block and know that the result is ready. You can call the get() function with a timeout, or call isDone() first to see if the task is completed, and then call get().