java多线程(3)

201 阅读5分钟

在前文中我们可以得知使用ThresdPoolExecutor的execute方法,是不能够获取线程的执行结果的,那么如何获取返回值呢?future 和FutureTask是java用以完成这一要求的功能。

1 . Future

首先我们可以看到Future是 java.util.concurrent包下面的接口,他包括了这么几个方法:

//取消任务的方法
boolean cancel(boolean mayInterruptIfRunning);
//判断任务是否已经取消的方法
boolean isCancelled();
//判断任务是否已经结束的方法
boolean isDone();
//获取任务执行结果的方法
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;

因为Future是一个接口,这里我们可以看到重点的实现类有这么几个:FutureTask 以及AbstractFuture

AbstractFuture的实现与FutureTask的实现非常相似,FutureTask使用unsafe包的CAS实现,而AbstractFuture使用的是AQS(AbstractQueuedSynchronizer,jdk锁实现的模板类),AQS以及AbstractFuture会在更下面来讨论,这里先讲解FutureTask的实现

1.1 FutureTask

FutureTask<V> implements RunnableFuture

FutureTask直接实现了一个叫做RunnableFuture的接口

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

通过RunnableFuture的代码可以看到实际上这个接口是对Runnable, Future这两个接口的一个包装,实际上就是在成功执行run()方法后,可以通过Future访问执行结果。这样FutureTask就同时可以包装 runnable和callable对象以及被传递到Excetor去执行。简单写个例子:

public class FutureTest {
    //线程池
    private final static ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.MINUTES,
            new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Future <String>future1 = executorService.submit(() -> {
            System.out.println("start four " + Thread.currentThread().getName());
            return "one ";
        });
        Future future2 = executorService.submit(() -> System.out.println("start  two " + Thread.currentThread().getName()));
        Future future3 = executorService.submit(() -> System.out.println("start  three " + Thread.currentThread().getName()));
        Future<String> future4 = executorService.submit(() -> {
            System.out.println("start four " + Thread.currentThread().getName());
            return "four ";
        });
        System.out.println("task one res:" + future1.get() + Thread.currentThread().getName());
        System.out.println("task two res:" + future2.get() + Thread.currentThread().getName());
        System.out.println("task three res:" + future3.get() + Thread.currentThread().getName());
        System.out.println("task four res:" + future4.get() + Thread.currentThread().getName());
        executorService.shutdown();//关闭线程池,阻塞直到所有任务执行完毕
    }
}

执行结果如下 屏幕快照 2022-01-28 下午3.55.25.png FutureTask的核心方法如下: FutureTask的两个构造方法:

/**
 *FutureTask 实现Callable
 */
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

/**
 * 实现Callable 并返回结果
 */
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

public boolean isCancelled() {
    return state >= CANCELLED;
}

public boolean isDone() {
    return state != NEW;
}

简单讲一个核心的方法:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

首先判断 state状态,如果状态小于COMPLETING也就是处于NEW状态,就调用awaitDone方法

 * 可能的执行顺序
 * NEW -> COMPLETING -> NORMAL
 * NEW -> COMPLETING -> EXCEPTIONAL
 * NEW -> CANCELLED
 * NEW -> INTERRUPTING -> INTERRUPTED
 */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

awaitDone方法 中存在一个无限循环方法 判断state和等待队列的状态queued,还有等待节点WaitNode

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

这个等待节点的操作和判断是用UNSAFE.compareAndSwapObject(cas方法)来进行判断的,cas方法前文有叙述过,这里就不再赘述了。如果时间超过deadline就直接返回状态结果,如果没有则调用LockSupport方法创建locks的基本线程阻塞基元,如果有时间单位就给这个阻塞加上时间,没有就直接产生阻塞

/**
  简单的用一个栈存储等待中的线程节点
 */
static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

通过状态判断以后执行report方法:

@SuppressWarnings("unchecked")
private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

返回具体线程的执行状态。

Future 的优缺点

优点

  • 1、可配合线程池使用,也可以单独创建,适合使用在经典的线程池中。 一些缺点:
  • 1、没有提供通知的机制,导致无法得知future何时完成
  • 2、如果使用阻塞的future.get。等待future返回的结果其实是同步操作,使用isDone()轮询判断的话,会耗费cpu的资源

1.2 ComplteableFuture

java 8为了解决FutureTask的一些性能问题, 引入了ComplteableFuture ,解决了FutureTask中许多的问题,并且吸收了所有Google Guava中ListenableFuture和SettableFuture的特征,还提供了其它强大的功能,让Java拥有了完整的非阻塞编程模型。

CompletableFuture 实现了 FutureCompletionStage两个接口,

class CompletableFuture<T> implements Future<T>, CompletionStage<T

Future 的接口上文已经叙述了,这里不再概述,首先来看CompletionStage接口:

CompletionStage的方法命名

CompletionStage接口有三十多个方法,其实这么多的方法都是实现了组合逻辑: 正常处理的异步计算的逻辑分为三种:

  • 前一个计算完成以后再调度第二个计算, 也就是then的逻辑
  • 两个计算都完成以后再进行下一个计算, 也就是both的逻辑
  • 两个计算其中一个完成以后就进行下一个计算, 也就是either的逻辑
  • 方法中如果含有apply字样就意味这个组合的方式是function,即接受前一个计算的结果,并且计算以后返回一个新的结果。
  • 方法中如果含有accept字样就意味这组合的方式是consumer,即接受前一个计算的结果,并且计算以后不返回有意义的值。
  • 方法中如果含有run字样就意味这个组合的方式是runnable,即忽略一个计算的结果,仅仅等待前一个计算完成以后才开始下一个计算

大概总结一下就是这样一个表格:

thenbotheither
applythenApplythenCombineapplyToEither
acceptthenAcceptthenAcceptBothacceptEither
runthenRunthenAfterBothrunAfterEither

这就有了基础的9个方法,同时CompletionStage还拥有这些个方法的async版本:比如thenCombineAsync

public <U,V> CompletableFuture<V> thenCombine(
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(null, other, fn);
}

public <U,V> CompletableFuture<V> thenCombineAsync(
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(asyncPool, other, fn);
}

较代码上来看他们的差别就主要在于asyncPool的使用:

private static final boolean useCommonPool =
    (ForkJoinPool.getCommonPoolParallelism() > 1);

/**
 * Default executor -- ForkJoinPool.commonPool() unless it cannot
 * support parallelism.
 */
private static final Executor asyncPool = useCommonPool ?
    ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
   

可以看到如果说在有Async后缀的版本并且不传 Executor的话,方法就会使用默认的ForkJoinPool#commonPool

  • 没有 Async 后缀的版本,后一个计算将在前一个计算完成时紧接着进行。这里有一个非常不幸的不确定性,如果前一个计算在 thread-1 上进行,执行组合方法(例如 thenApply),如果前一个计算在执行组合方法时还没有完成,那么完成后后一个计算就在 thread-1 上紧接着进行。如果在执行组合方法时前一个计算已经完成,那么后一个计算将在执行组合方法的线程上执行。
  • 有 Async 后缀但不传递 Executor 的版本,后一个计算将在前一个计算完成时由默认的 Executor 执行。这个 Executor 会是 ForkJoinPool#commonPool,但如果这个线程池不支持并发也就是并发度不大于 1 的话,就会为每个计算都起一个新的 Thread 来执行。
  • 有 Async 后缀且传递 Executor 的版本,跟不传递的版本类似,只是使用用户传递的 Executor 来执行。

CompletableFuture中的异常处理

失败的异步处理这里有三个接口:

public CompletionStage<T> whenComplete
        (BiConsumer<? super T, ? super Throwable> action);

public <U> CompletionStage<U> handle
        (BiFunction<? super T, Throwable, ? extends U> fn);

public CompletionStage<T> exceptionally
        (Function<Throwable, ? extends T> fn);

描述性地说,whenComplete 在方法结束后插入一个动作,并返回原来的计算结果;handle 在方法结束后插入一个函数,返回一个可能是新的类型的计算结果;exceptionally 针对失败的计算的具体异常,返回一个和原先计算结果类型相符合的值,其语义类似于从异常中恢复。 这里使用一个实例代码来看看具体的运行结果

public static void main(String[] args) throws Exception {
    // 创建异步执行任务:
    CompletableFuture<Double> cf = CompletableFuture.supplyAsync(CompletableFutureTest::fetchPrice);
    // 如果执行成功:
    cf.thenAccept((result) -> {
        System.out.println("price: " + result);
    });
    // 如果执行异常:
    cf.exceptionally((e) -> {
        System.out.println("exceptionally: " + e);
        return null;
    });
    cf.whenComplete((result,e) -> {
        System.out.println("Complete: " + e);
    });
    cf.handle((result,e) -> {
        System.out.println("handle: " + e);
       return null;
    });
    // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
    Thread.sleep(200);
}

static Double fetchPrice() {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
    }
    if (Math.random() < 300) {
        throw new RuntimeException("fetch price failed!");
    }
    return 5 + Math.random() * 20;
}

正常的结果:

屏幕快照 2022-02-02 下午5.04.12.png

异常的结果:

屏幕快照 2022-02-02 下午5.02.54.png 可以看到exceptionally是专门对异常进行处理的,而另两个兼顾了返回的结果

runAsyc 和supplyAync 测试代码:

public static void main(String[] args) {
    try {
    CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第一个异步任务");
    });
        f1.get();

    CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第二个异步任务");
        return  "second";
    });
        CompletableFuture.allOf(f1,f2).join();
        System.out.println("第一个异步任务返回"+f1.get());
        System.out.println("第二个异步任务返回"+f2.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println("CompletableFuture Test runAsync");
}