Explicación detallada de ejemplos de orquestación asincrónica multiproceso

1. Revisión de subprocesos múltiples

Tabla de contenido

1. Revisión de subprocesos múltiples

1. 4 formas de inicializar hilos

2. Siete parámetros del grupo de subprocesos

3. Cuatro tipos comunes de grupos de subprocesos

4. ¿Por qué utilizar el grupo de subprocesos en el desarrollo?

2. Orquestación asincrónica CompletableFuture

1. Crea un objeto asincrónico

2. Método de devolución de llamada cuando se completa el cálculo 

Tres, método de manejo

4. Método de serialización de subprocesos

5. Dos combinaciones de tareas: ambas deben completarse

6. Combinación de dos tareas: una completada

7. Combinación multitarea


 

1. 4 formas de inicializar hilos

1), heredar hilo

Thread thread = new Thread01();
thread.start();

2), implementar la interfaz Runnable

Runnable01 runnable01 = new Runnable01();
new Thread(runnable01).start();

3) Implemente la interfaz Callable + FutureTask (puede obtener el resultado devuelto y manejar excepciones)

FutureTask futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
// 阻塞等待线程执行完成,获取返回结果
Integer i = (Integer) futureTask.get();

4), grupo de subprocesos

public static ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new Runnable01());

        Inicialice el grupo de subprocesos de las dos formas siguientes:

Executors.newFiexedThreadPool(3);
//或者
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, 
Diferencia:
 Thread y Runnable no pueden obtener el valor de retorno, y Callable puede obtener el valor de retorno. 
Thread, Runnable y Callable no pueden controlar los recursos, pero el grupo de subprocesos puede controlar los recursos. 
A través del grupo de subprocesos, el rendimiento es estable y los resultados de la ejecución pueden También se pueden obtener datos y se pueden detectar excepciones. Sin embargo, en situaciones comerciales complejas, una llamada asincrónica puede depender del resultado de la ejecución de otra llamada asincrónica.

2. Siete parámetros del grupo de subprocesos

/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

Siete parámetros básicos: 

corePoolSize: la cantidad de subprocesos principales, la cantidad de subprocesos que están listos después de que se crea el grupo de subprocesos y esperan recibir tareas asincrónicas.

MaximumPoolSize: número máximo de subprocesos, recursos de control

keepAliveTime: tiempo de supervivencia. Si el número actual de subprocesos es mayor que el número de subprocesos principales, si el tiempo de inactividad del subproceso es mayor que el keepAliveTime especificado, el subproceso se liberará (número máximo de subprocesos - número de subprocesos principales)

unidad: unidad de tiempo

BlockingQueue<Runnable> workQueue: cola de bloqueo. Si hay demasiadas tareas, la cantidad actual de tareas se colocará en la cola (las tareas enviadas mediante Runable se ejecutan). Cuando hay subprocesos inactivos, las tareas se sacarán de la cola y se ejecutarán.

ThreadFactory: Fábrica para crear hilos.

Controlador RejectedExecutionHandler: si la cola está llena, rechace ejecutar la tarea de acuerdo con la política de rechazo especificada

Cola ilimitada y cola limitada en cola de bloqueo:

Cola acotada : es una cola con un tamaño fijo. Por ejemplo, se establece un LinkedBlockingQueue con un tamaño fijo, o un SynchronousQueue con un tamaño de 0 solo se usa para la transferencia entre productores y consumidores.

Cola ilimitada : se refiere a una cola que no tiene un tamaño fijo. La característica de estas colas es que se pueden poner en cola directamente hasta que se desborden. Por supuesto, la realidad rara vez tiene una capacidad tan grande (que excede Integer.MAX_VALUE), por lo que desde la experiencia del usuario, es equivalente a "ilimitado". Por ejemplo, no existe un conjunto LinkedBlockingQueue de tamaño fijo.

*Proceso en ejecución:

1. Cree un grupo de subprocesos, prepare una cantidad central de subprocesos principales y prepárese para aceptar tareas.

2. Cuando llegan nuevas tareas, se ejecutan utilizando subprocesos inactivos preparados por el núcleo.

        (1) Si el núcleo está lleno, las tareas entrantes se colocarán en la cola de bloqueo. El núcleo inactivo bloqueará la cola para obtener la ejecución de la tarea.

        (2) Cuando la cola de bloqueo esté llena, se abrirá un nuevo hilo directamente para su ejecución. El número máximo solo se puede abrir hasta el número especificado por max.

        (3) y max se ejecutan todos. El número máximo de subprocesos inactivos se destruirá automáticamente después del tiempo especificado por keepAliveTime. Finalmente mantenido al tamaño del núcleo.

        (4) Si el número de subprocesos alcanza el número máximo y llegan nuevas tareas, se utilizará para el procesamiento la política de rechazo especificada por rechazo.

3. Todos los hilos son creados por la fábrica especificada.

3. Cuatro tipos comunes de grupos de subprocesos

newCachedThreadPool: crea un grupo de subprocesos almacenable en caché. Si la longitud del grupo de subprocesos excede las necesidades de procesamiento, los subprocesos inactivos se pueden reciclar de manera flexible. Si no hay forma de reciclar, se creará un nuevo subproceso.

newFixedThreadPool: crea un grupo de subprocesos de longitud fija que puede controlar el número máximo de subprocesos simultáneos. Los subprocesos que excedan esperarán en la cola.

newScheduledThreadPool: crea un grupo de subprocesos de longitud fija para admitir la ejecución de tareas periódicas y programadas.

newSingleThreadExecutor: crea un grupo de subprocesos de un solo subproceso, que solo utilizará un subproceso de trabajo único para ejecutar tareas, asegurando que todas las tareas se ejecuten en el orden especificado (FIFO, LIFO, prioridad)

4. ¿Por qué utilizar el grupo de subprocesos en el desarrollo?

  • Reducir el consumo de recursos: reducir la pérdida causada por la creación y destrucción de subprocesos mediante la reutilización de subprocesos ya creados.
  • Mejorar la velocidad de respuesta: porque cuando el número de subprocesos en el grupo de subprocesos no excede el límite máximo del grupo de subprocesos, algunos subprocesos están en estado de espera de las tareas asignadas, cuando llegan las tareas, se pueden ejecutar sin crear nuevos subprocesos. .
  • Mejorar la capacidad de administración de subprocesos: el grupo de subprocesos optimizará los subprocesos en el grupo en función de las características actuales del sistema para reducir la sobrecarga del sistema causada por la creación y destrucción de subprocesos. La creación y destrucción ilimitada de subprocesos no solo consume recursos del sistema, sino que también reduce la estabilidad del sistema. Utilice el grupo de subprocesos para una asignación unificada.

2. Orquestación asincrónica CompletableFuture

        Escenario empresarial: la lógica de consultar la página de detalles del producto es relativamente compleja y algunos datos deben llamarse de forma remota, lo que inevitablemente llevará más tiempo.

5ca33d7bce6d4506a6050cc827358db7.png

       Si cada consulta en la página de detalles del producto requiere que se complete el tiempo marcado a continuación, el usuario tardará 5,5 segundos en ver el contenido de la página de detalles del producto. Obviamente esto es inaceptable. Si varios subprocesos completan estos 6 pasos al mismo tiempo, es posible que solo se necesiten 1,5 segundos para completar la respuesta.


        Future es una clase agregada en Java 5 para describir el resultado de un cálculo asincrónico. Puede usar el método `isDone` para verificar si el cálculo se completó, o usar `get` para bloquear el hilo de llamada hasta que se complete el cálculo y devolver el resultado. También puede usar el método `cancel` para detener la ejecución de la tarea.

         Aunque "Future" y los métodos de uso relacionados brindan la capacidad de ejecutar tareas de forma asincrónica, es muy inconveniente obtener los resultados, ya que los resultados de las tareas solo se pueden obtener mediante bloqueo o sondeo. El método de bloqueo es obviamente contrario a la intención original de nuestra programación asincrónica. El método de sondeo consume recursos innecesarios de la CPU y no puede obtener los resultados del cálculo a tiempo. ¿Por qué no podemos usar el patrón de diseño del observador para notificar al oyente a tiempo cuando el cálculo? ¿Se completan los resultados? ¿Paño de lana?

         Muchos lenguajes, como Node.js, utilizan devoluciones de llamada para implementar programación asincrónica. Algunos marcos de Java, como Netty, extienden la interfaz `Future` de Java y proporcionan múltiples métodos de extensión como `addListener`; Google guava también proporciona un Future extendido general; Scala también proporciona una programación asincrónica Future/Promise potente y fácil de usar. modelo.

        Como biblioteca de clases Java ortodoxa, ¿deberíamos hacer algo para mejorar las funciones de nuestra propia biblioteca?

        En Java 8, se agregó una nueva clase que contiene alrededor de 50 métodos: CompletableFuture, que proporciona funciones de extensión Future muy poderosas, puede ayudarnos a simplificar la complejidad de la programación asincrónica y proporciona capacidades de programación funcional a través de devoluciones de llamadas. Los resultados del cálculo se procesan en un manera, y se proporcionan métodos para convertir y combinar CompletableFuture. La clase CompletableFuture implementa la interfaz Future, por lo que aún puedes obtener los resultados bloqueando o sondeando con el método `get` como antes, pero este método no se recomienda.

        CompletableFuture y FutureTask pertenecen a la clase de implementación de la interfaz Future y ambos pueden obtener los resultados de ejecución del hilo.

5329b3b6a74849139d7d57b8824a7687.png

1. Crea un objeto asincrónico

CompletableFuture proporciona cuatro métodos estáticos para crear una operación asincrónica.

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)

1. runXxxx no devuelve resultados, pero SupplyXxx puede obtener resultados de devolución.

2. Puede pasar un grupo de subprocesos personalizado; de lo contrario, se utilizará el grupo de subprocesos predeterminado;

2. Método de devolución de llamada cuando se completa el cálculo 

public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

whenComplete puede manejar resultados de cálculo normales y anormales (devolución de llamada exitosa) y excepcionalmente maneja situaciones anormales (devolución de llamada de excepción).

La diferencia entre whenComplete y whenCompleteAsync:

         whenComplete: el hilo que ejecuta la tarea actual continúa ejecutando la tarea de whenComplete. 

        whenCompleteAsync: la ejecución continúa enviando la tarea whenCompleteAsync al grupo de subprocesos para su ejecución.

El método no termina con Async, lo que significa que la Acción usa el mismo subproceso para ejecutarse, y Async puede usar otros subprocesos para ejecutarse (si usa el mismo grupo de subprocesos, también puede ser seleccionado por el mismo subproceso para su ejecución)

public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("Main...Start...."+Thread.currentThread());
        // supplyAsync 可获取返回值
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程" + Thread.currentThread());
            int i = 10 / 0;
            System.out.println("运行结果:" + i);
            return i;
        }, threadPool).whenCompleteAsync((res,exception)->{
            // 当上面的任务执行完成,能得到结果和异常信息,但无法修改返回值
            System.out.println("异步任务完成,结果为:"+res+",异常为:"+exception);
        }).exceptionally(throwable -> {
            // 可以感知异常,同时返回默认值
            return 101;
        });
        Integer integer = result.get();
        System.out.println("Main...End...."+integer);

    }

resultado:

Main...Start....Thread[main,5,main]
当前线程Thread[pool-1-thread-1,5,main]
异步任务完成,结果为:null,异常为:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
Main...End....101

Similar a entonces y rechazo de Promise en ES6;

Tres, método de manejo

public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 方法执行完成后的处理
         */
        System.out.println("Main...Start...."+Thread.currentThread());
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程" + Thread.currentThread());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }, threadPool).handle((res,throwable)->{
            // handle可获取到返回值并可进行修改,且可以感知异常,并修改返回值
            if(res!=null && throwable==null){
                return res;
            }else{
                return 0;
            }
        });
        Integer integer = result.get();
        System.out.println("Main...End...."+integer);

    }

resultado:

Main...Start....Thread[main,5,main]
当前线程Thread[pool-1-thread-1,5,main]
Main...End....0

4. Método de serialización de subprocesos

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor)


public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor)

Método thenApply: cuando un hilo depende de otro hilo, obtiene el resultado devuelto por la tarea anterior y devuelve el valor de retorno de la tarea actual.

método thenAccept: consume los resultados del procesamiento. Reciba los resultados del procesamiento de la tarea y consuma el procesamiento sin devolver ningún resultado.

Método ThenRun: siempre que se complete la tarea anterior, se ejecutará ThenRun y ​​solo después de procesar la tarea se ejecutarán las operaciones posteriores de ThenRun.

Con Async, la ejecución es asíncrona de forma predeterminada. Igual que antes.

Las tareas de requisitos previos anteriores deben completarse con éxito.

Función<? ¿Súper T? extiende U>

        T: El tipo de resultado devuelto por la tarea anterior.

        U: el tipo de valor de retorno de la tarea actual

public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 线程串行化
         *  1、thenRunAsync:无法获取上一步的执行结果,无返回值
         *  .thenRunAsync(() -> {
         *             System.out.println("任务2启动了");
         *         },threadPool)
         *  2、thenAcceptAsync:能获取到上一步的返回值,无返回值
         *  .thenAcceptAsync(res -> {
         *             System.out.println("任务2启动了"+res);
         *         },threadPool)
         *  3、thenApplyAsync:既能获取到上一步的返回值,有返回值
         */
        System.out.println("Main...Start...."+Thread.currentThread());
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程" + Thread.currentThread());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }, threadPool).thenApplyAsync(res->{
            System.out.println("任务2启动了"+res);
            return "hello,"+res;
        },threadPool);
        //future.get() 获取返回值,会阻塞
        System.out.println("Main...End...."+future.get());

    }

resultado:

Main...Start....Thread[main,5,main]
当前线程Thread[pool-1-thread-1,5,main]
运行结果:5
任务2启动了5
Main...End....hello,5

5. Dos combinaciones de tareas: ambas deben completarse

public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn, Executor executor)
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action, Executor executor)
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor)

Ambas tareas deben completarse para activar esta tarea.

thenCombine: combina dos futuros, obtiene los resultados de retorno de los dos futuros y devuelve el valor de retorno de la tarea actual.

 thenAcceptBoth: combina dos futuros, obtiene los resultados de retorno de las dos tareas futuras y luego procesa las tareas sin devolver un valor.

runAfterBoth: combina dos futuros, no es necesario obtener el resultado del futuro, solo necesita procesar la tarea después de que los dos futuros hayan procesado la tarea.

Ejemplo :

Primero crea dos tareas.

/**
 * 两个都完成
 */
System.out.println("Main...Start...."+Thread.currentThread());
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
    System.out.println("任务1开始" + Thread.currentThread().getId());
    int i = 10 / 2 ;
    System.out.println("任务1结束");
    return i;
}, threadPool);

CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
    System.out.println("任务2开始"+Thread.currentThread().getId());
    System.out.println("任务2结束");
    return "Hello";
}, threadPool);

Utilice runAfterBothAsync (no se pueden obtener los resultados de la ejecución de la tarea anterior):

future01.runAfterBothAsync(future02,()->{
    System.out.println("任务3启动");
},threadPool);

// 结果:

Main...Start....Thread[main,5,main]
任务1开始12
任务1结束
任务2开始13
任务2结束
任务3启动

Utilice thenAcceptBothAsync (los resultados de la ejecución de la tarea anterior se pueden obtener, pero no se pueden devolver):

future01.thenAcceptBothAsync(future02,(f1,f2)->{
    System.out.println("任务3开始----f1结果:"+f1+"===f2结果"+f2);
},threadPool);

// 结果:

Main...Start....Thread[main,5,main]
任务1开始12
任务1结束
任务2开始13
任务2结束
任务3开始----f1结果:5===f2结果Hello

Utilice luegoCombineAsync (puede obtener los resultados de la ejecución de tareas anteriores, modificarlos y devolver los resultados):

CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
    return f1 +":"+ f2 + "--->Hello";
}, threadPool);
System.out.println("Main...End...."+future.get());

// 结果:
Main...Start....Thread[main,5,main]
任务1开始12
任务1结束
任务2开始13
任务2结束
Main...End....5:Hello--->Hello

6. Combinación de dos tareas: una completada

public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor)
public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,Executor executor)
public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn,Executor executor)

Cuando se completa cualquiera de las tareas futuras de las dos tareas, se ejecuta la tarea.

applyToEither: una de las dos tareas se completa, obtiene su valor de retorno, procesa la tarea y tiene un nuevo valor de retorno. aceptar cualquiera: una de las dos tareas se completa, obtiene su valor de retorno y procesa la tarea, no hay ningún nuevo valor de retorno. runAfterEither: una de las dos tareas se completa, no es necesario obtener el resultado del futuro, procesar la tarea y no hay valor de retorno.

7. Combinación multitarea

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

allOf: espera a que se completen todas las tareas

anyOf: siempre que se complete una tarea

Ejemplo:

Cree tres tareas para simular la adquisición de datos cuando los usuarios cargan la página de inicio.

CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
    System.out.println("查询商品图片信息");
    return "hello.jpg";
}, threadPool);

CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
    System.out.println("查询商品属性");
    return "白色+1TB";
}, threadPool);

CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
    System.out.println("查询商品介绍");
    return "Apple";
}, threadPool);

Utilice todo de:

CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);

allOf.get(); // 等待所有任务执行完成 返回结果

// 获取所有任务的执行结果
System.out.println("Main...End...."+futureImg.get()+"===>"+futureAttr.get()+"===>"+futureDesc.get());

// 结果:
Main...Start....Thread[main,5,main]
查询商品图片信息
查询商品属性
查询商品介绍
Main...End....hello.jpg===>白色+1TB===>Apple

Utilice cualquiera de:

CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get(); // 有一个任务执行完成 返回结果

// 获取所有任务的执行结果
System.out.println("Main...End...."+anyOf.get());

// 结果:
Main...Start....Thread[main,5,main]
查询商品图片信息
查询商品属性
Main...End....hello.jpg
查询商品介绍

 

 

Supongo que te gusta

Origin blog.csdn.net/weixin_53922163/article/details/128177316
Recomendado
Clasificación