Future和ExecutorCompletionService

1、线程池中采用submit方法执行

为什么要来记录一下这个方法呢,主要是我在测试代码中写了一个bug,代码如下所示:

public class FutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<String> submit = executorService.submit(() -> {
            TimeUnit.SECONDS.sleep(10);
            return "hello,world";
        });
        System.out.println("代码执行啦,准备获取得到结果");
        String s = submit.get();
        System.out.println(String.format("获取得到的结果是:%s",s));
    }
}

主要是因为使用了submit方法之后,主线程想要获取得到执行的值,结果发现主线程一直阻塞在获取得到结果。

所以来探究一下具体的实现过程

submit源代码

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

可以看到submit的本质上还是调用了execute方法来执行任务。

这里直接创建了一个任务,那么有必要掰扯一下这个任务对象了。

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

看一下构造函数:

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        // 作为任务
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

看一下继承体系结构

之所以可以用execute方法来进行执行,是因为FutureTask也是一个Runnable接口

那么看一下具体的执行代码:

因为这里是在线程池中来进行执行的,所以只看一下关键代码即可。最终需要调用对象的task.run();

执行FutureTask对象的run方法

    public void run() {
        // 看一下这里的thread设置的值
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    // 让线程执行并返回对应的结果
                    result = c.call();
                    // 如果任务执行结束,那么这里做个标记
                    ran = true;
                } catch (Throwable ex) {
                    // 如果执行代码出现了异常,结果置空,并设置异常
                    result = null;
                    ran = false;
                    setException(ex);
                }
                // 如果任务正常完成,那么需要唤醒阻塞的线程
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

这里思考一下,为什么要唤醒阻塞的线程?线程为什么会阻塞?

因为主线程一直在获取得到结果,而对于获取不到结果的线程来说,会阻塞,那么是怎么阻塞的?

这个得看下get方法中的阻塞逻辑。

唤醒阻塞线程之set方法

那么看一下唤醒阻塞线程的set方法:

    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            // 最终调用结束方法
            finishCompletion();
        }
    }

完成任务唤醒被阻塞线程finishCompletion方法

看下方法:

    private void finishCompletion() {
        // assert state > COMPLETING;
        // 只有get方式的时候,才会执行到这一步,所以先去看下get方法
        for (WaitNode q; (q = waiters) != null;) {
            // 比较会置换这里的值
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        // 任务置空并唤醒阻塞的代码
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    // 循环置换
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }

        done();

        callable = null;        // to reduce footprint
    }

call方法执行中出现异常setException(ex)方法

可以看到正常执行和执行任务期间出现了问题调用方法对象不同:

    protected void setException(Throwable t) {
        // 异常也会设置完成标识
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            // 设置最终的状态为异常。
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            // 最终唤醒线程
            finishCompletion();
        }
    }

get方法

上面是唤醒代码,那么下面就应该是阻塞代码了。

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // 等待代码。只有state为NORMAL以及NORMAL之后的才会获取到结果
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

那么如果任务还没有执行完成,那么就需要阻塞来得到结果。具体的阻塞代码就在awaitDone方法中

awaitDone方法

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;
	    // 这里有几种情况,如果真的成为了NORMAL,那么表示执行完成了
        // 但是如果是异常等情况,那么再次判断q,看看是不是因为下面循环引起的
        // q只有在第二轮循环之后才有值
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        // 第二轮循环中肯定是要将其置为COMPLETING状态了,那么这里将会造成调用get方法的线程让步执行
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        // 第一轮循环肯定是null,创建新的节点
        else if (q == null)
            q = new WaitNode();
        // 在有一轮执行到这里,那么表示waiters是q
        else if (!queued)
            // 执行完成返回true
            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
            // 因为是false,所以会阻塞
            // 状态是NEW的时候将会阻塞在这里
            LockSupport.park(this);
    }
}

report方法

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);
}

2、问题

首先看下我的代码的打印输出:

pool-1-thread-2
pool-1-thread-1
submit最终的结果是:1
submit1最终的结果是:1

第一行先出来,但是隔了很久之后,后面三行瞬间出来,那么为什么会出现这样的效果呢?

我通过源码定位之后发现问题主要是在get方法这里:

            Integer integer1 = submit.get();
            Integer integer2 = submit1.get();

提交任务之后,创建FutureTask之后,就来进行执行,然后get方法异步等待结果。

但是因为第一行代码中的任务执行时间比较长,而get导致main线程一直在进行等待。而此时,第二个任务已经执行完成了(可以通过打印完成之间看看),但是得等到通知第二个get方法,但是第二个方法还没有阻塞住,也就是说还没有进入到循环中去。

所以等到第一个get方法执行结束之后,第二个方法才会来继续执行。

第一个代码任务执行时间比较长,而第二个get方法执行比较短。虽然第二个任务早已经执行完毕,但是仍然需要等待第一个任务执行结束。

那么这种方式使用起来感觉非常low,那么有什么好的解决方式呢?当然是有的,但是需要根据实际情况来进行考虑

如果说不一定要拿到任务执行的结果,那么可以考虑另外一个get超时API;如果说一定要拿到结果且保证异步执行,那么使用另外一个工具类

使用Callable和Future有没有产生新的线程?没有,这里只是作为任务来进行执行。

3、ExecutorCompletionService

利用ExecutorCompletionService来解决上面get阻塞时阻塞在调用者线程的问题。

那么看下对应的解决代码:

public class FutureTestPro {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,12,5, TimeUnit.SECONDS,new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        ExecutorCompletionService<Integer> completionService = new ExecutorCompletionService<>(threadPoolExecutor);
        completionService.submit(()->myTask1());
        completionService.submit(()->myTask2());
        try {
            Integer integer = completionService.take().get();
            System.out.println(integer);
            integer = completionService.take().get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        threadPoolExecutor.shutdownNow();

    }

    public static int myTask1(){
        try {
            Thread.sleep(10000);
            System.out.println("task1----->>"+Thread.currentThread().getName());
            return 1;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return 0;
        }
    }

    public static int myTask2(){
        try {
            Thread.sleep(3000);
            System.out.println("task2---->>>"+Thread.currentThread().getName());
            return 1;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return 0;
        }
    }
}

控制台打印:

task2---->>>pool-1-thread-2
1
task1----->>pool-1-thread-1
1

因为任务2执行时间比较短,任务1执行时间比较长。所以这里先打印出来的是任务2.

实现原理

CompletionService接口的唯一实现类ExecutorCompletionService,主要是因为ExecutorCompletionService实现了Future中的done方法。

那么慢慢看对应的原理:

public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;
    private final AbstractExecutorService aes;
    // 主要是因为在这里添加了一个阻塞队列,将任务的结果保存起来,哪个限制性,哪个放在队头
    private final BlockingQueue<Future<V>> completionQueue;
 	..........   
}

先看下上面的使用过程中的操作:

    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        // 传入的实现类要是AbstractExecutorService的子类,否则这个线程池就为null
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        // 默认的阻塞队列是LinkedBlockingQueue
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

ExecutorCompletionService的submit方法

看下具体的方法:

    public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        // 看下重写的任务
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
    }

重写任务:

    private RunnableFuture<V> newTaskFor(Callable<V> task) {
        if (aes == null)
            // 如果线程池为空
            return new FutureTask<V>(task);
        else
            // 线程池不为空的时候,也是上面的操作
            return aes.newTaskFor(task);
    }

看下对应的执行方法【重点】:

    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        // 真正核心的代码,在Future中的代码运行结束之后,这里会将先执行完成的任务添加到队列中来
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }

利用多态的特性,最终执行的将会是这里的代码。

因为在Future中的finishCompletion方法中

    private void finishCompletion() {
 		// 在通知的过程中说明已经有结果了,那么将结果保存起来
        // 这里就是上面重写的目的
        done();

        callable = null;        // to reduce footprint
    }

将结果保存到阻塞队列中即可。

take方法

阻塞队列的take方法,这个是最为常见的了。

    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }

直接从阻塞队列中来进行获取得到对应的值。

posted @ 2022-11-01 23:07  写的代码很烂  阅读(8)  评论(0编辑  收藏  举报