java源码浅析之FutureTask

前言

无聊看看源码吧,写成博客加深印象

FutureTask

先复习一下怎么使用的

        FutureTask<Integer> task = new FutureTask<>(() -> {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("正在执行计算");
            return 100;
        });
        new Thread(task, "t3").start();

        Integer result = null;
        try {
            result = task.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("结果是:"+result);
    }

即task.get()方法会阻塞调用方法的线程,直到task运行结束

我看源码前习惯先去思考如果是我应该怎么实现上述效果?

  • 调用get方法时,如果task还没结束,则使用park方法阻塞调用的线程
  • task执行完后,将结果存入某个中间变量,并且调用unpark方法唤醒阻塞的线程
  • 阻塞线程被唤醒后,从中间变量获取结果

那么看源码的时候就带着上述思路看看是否大佬也是怎么做的
首先我们看看get方法

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
  • state是一个volatile修饰的可见变量,用来表示task的状态
  • get方法先判断state是否正在运行,s<=COMPLETING表示还在运行,大于表示终止
  • 那么就调用awaitDone来阻塞当前线程
  • 如果已运行完,则调用report获取中间变量返回

接着我们看看awaitDone是怎么阻塞的
(跟着序号去看源码注释)
为了更好地理解这个阻塞的过程,我进行一个补充

  • 学习AQS我们知道,一般多线程的阻塞都会设计一个阻塞队列来让需要等待的队列排队等待被唤醒
  • 那么AQS是链表,这里是什么呢
  • 答案是栈(Treiber stack of waiting threads 这是官方对waiters的解释)
  • 栈的结构是什么呢?成员变量waiters是什么呢
  • 栈的结构是一个链表,waiters是一个栈的head指针
  • 因此阻塞队列的样子应该是这样的 waiters-》t1WaitNode-》t2WaitNone
  • WaitNode里面存的是threadId
  • 因此需要唤醒的时候,只需要从waiters开始,不断地遍历next,找出其线程号进行Unpark就行了
  • 那么对于这个数据结构,怎么添加新的Node q呢
  • 即 q.next=waiters,waiters=q
  • 这对应下面代码段的
  • UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q)
  • 理解了q和阻塞队列的结构,下面的代码就很好理解了
    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;
            // 2、判断当前是否为运行态,大于COMPLETING为终止态
            // 若为终止态则线程为空
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            // 3、COMPLETING表示计算完了正在把结果放入中间变量
            // 因此只需使用yield让一下时间片即可,不需阻塞了
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            // 1、发现q为空,新建一个Node
            else if (q == null)
                q = new WaitNode();
            // 4、queued表示是否把节点丢进栈,还没丢则丢进去
            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);
            }
            // 5、节点丢进栈后,调用Park进行阻塞,等待被唤醒
            else
                LockSupport.park(this);
        }
    }

至此,思路中的阻塞等待过程解析完毕
有几个关键点可以留意一下

  • 哪里体现并发了?
    • 使用了阻塞队列
    • 使用CAS修改链表状态
  • 有什么巧妙之处?
    • 可以发现作者使用一个whiletrue循环+多个ifelse来完成功能
    • 仔细想想是很巧妙的
    • 如果我们无论如何就直接新建节点开始阻塞会带来频繁的上下文切换
    • 作者这么写相当于建好节点后重新循环一次,如果这时候state表示task线程已经执行完毕了,那么就不用丢尽栈,直接可以返回了
    • 丢进栈也一样,重新再循环一次如果task运行结束就把节点线程变为null,也不用进入阻塞

那么阻塞已经看完了,接着就得考虑怎么唤醒了吧,那肯定是写在run方法里的,那么我们接着看run方法

   public void run() {
   		// 防止多线程重复运行
        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);
        }
    }
  • run方法相对简单就不解析了
  • 可以看到run方法中执行完call方法后得到结果,会将结果set
  • 那么可以猜测set中除了将结果放进中间变量,肯定还包括唤醒操作

那么我们看看set方法

    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
  • 通过CAS先修改state的状态,更新中间变量改一次状态
  • 放好后再改一次状态,对应的就是我们get方法中提到的当state==COMPLETING时表示正在放中间变量,稍微等一下就行了

那么finishCompletion就是唤醒等待的线程了

    private void finishCompletion() {
        // assert state > COMPLETING;
        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
    }
  • 整个流程就是从waiters开始遍历挨个唤醒即可
  • 就不一一解析了
  • 其他cancel等方法也差不多

我感觉JUC的类实现,只要AQS看熟了,其他的看得都很轻松

如果有喜欢我这种讲解方式的点个赞哈

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值