报告:风险环境和外挂类型关系密切 加速器外挂在模拟器环境最多
近日,国内领先的手游反外挂服务商网易易盾对外发布了《2019年手游反外挂报告》(以下将简称“报告”),依靠其丰富的数据和深入的洞察,报告全面地呈现了手游外挂的真实情况。本篇文章,将和大家解读下报告里提及的风险环境与外挂种类之间的关系。
风险环境是指具有一定开挂风险,或者可以为作弊提供便利的手机环境。随着手机ROOT难度的增加,多数外挂使用者选择了模拟器、免ROO框架或者云真机进行游戏,方便使用外挂作弊。
目前,主要的风险环境包括:
网易易盾在报告里收集分析了外挂使用者环境,包括自动点击、加速器、修改器外挂,其使用情况如下:
在自动点击外挂中,50%的比例是使用Hook框架,Hook相关UI方法进行自动点击;27%的比例是使用模拟器自动点击;使用云真机的比例为 16%(网易易盾认为云真机是模拟点击使用者的利器),其余是使用ROOT手机等方式。
在加速器外挂中,使用最多的是模拟器,达到94%。其次是多开器、云真机等。值得一提的是,模拟器中往往有集成的加速器,更有一些以加速为卖点的游戏模拟器,比如天天模拟器等。
修改器作弊方式需要ROOT权限,在高版本手机中,ROOT 权限难以获取,所以修改器使用免ROOT框架比例达到 78%,其余20%是模拟器。
此外,报告的检测数据还显示,2019 年,使用模拟器的人数占外挂总用户的55.6%,使用云真机的人数占外挂总用户的 28.9%。因此,在报告中网易易盾建议,为了保证手游安全,手游厂商最好对模拟器、云真机进行检测。
据悉,网易易盾一直在手游反外挂市场不断耕耘,密切关注着作弊手段的发展。2019 年,网易易盾覆盖终端数4.2 亿,帮助200+游戏防御外挂和抵御各种威胁242.25 亿次,涵盖多种游戏类型。因此,不论是可靠性,还是真实性,这份手游反外挂报告都值得一读。
以上为报告的部分内容,如需下载完整报告,请通过以下方式获得:
iOS逆向|HOOK原理,修改他人程序执行流程的本质
1.Executor框架简介
2.ThreadPoolExecutor详解
3.ScheduledThreadPoolExecutor详解
4.FutureTask详解
在Java中,使用线程来异步执行任务。Java线程的创建与销毁需要一定的开销,如果我们为每一个任务创建一个新线程来执行,这些线程的创建与销毁将消耗大量的计算资源。同时, 为每一个任务创建一个新线程来执行,这种策略可能会使处于高负荷状态的应用最终崩溃。
Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器
(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。这种两级调度模型的示意图如下图所示。
从图中可以看出,应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。
下面将分两部分来介绍Executor:Executor的结构和Executor框架包含的成员组件。
Executor框架主要由3大部分组成如下。
· 任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
· 任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
· 异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
Executor框架的类与接口
下面是这些类和接口的简介。
· Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开
来。
· ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
· ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执
行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
· Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
· Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled- ThreadPoolExecutor执行。
Executor框架的使用示意图如下图所示。
Executor框架的使用示意图
主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(Executor-
Service.submit(Runnable task)或ExecutorService.submit(Callable<T>task))。
如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象
(到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。
最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行
FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
本节将介绍Executor框架的主要成员:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors。
ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。
下面分别介绍这3种ThreadPoolExecutor。
下面是Executors提供的,创建使用固定线程数的FixedThreadPool的 API。
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactorythreadFactory
FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场
景,它适用于负载比较重的服务器。
下面是Executors提供的,创建使用单个线程的SingleThread-Executor的API。
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。
下面是Executors提供的,创建一个会根据需要创建新线程的CachedThreadPool的API。
public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
CachedThreadPool是大小的线程池,适用于执行很多的短期异步任务的小程序,或者 是负载较轻的服务器。
ScheduledThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建2种类
型的ScheduledThreadPoolExecutor,如下。
· ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。
· SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。下面分别介绍这两种ScheduledThreadPoolExecutor。
下面是工厂类Executors提供的,创建固定个数线程的ScheduledThreadPoolExecutor的API。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize,ThreadFactory)
ScheduledThreadPoolExecutor适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。下面是Executors提供的,创建单个线程的SingleThreadScheduledExecutor的API。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() public static ScheduledExecutorService newSingleThreadScheduledExecutor (ThreadFactory threadFactory)
SingleThreadScheduledExecutor适用于需要单个后台线程执行周期任务,同时需要保证顺
序地执行各个任务的应用场景。
Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。当我们把Runnable 接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们返回一个FutureTask对象。下面是对应的API。
<T> Future<T> submit(Callable<T> task)
<T> Future<T> submit(Runnable task, T result) Future<> submit(Runnable task)
有一点需要读者注意,到目前最新的JDK 8为止,Java通过上述API返回的是一个FutureTask对象。但从API可以看到,Java仅仅保证返回的是一个实现了Future接口的对象。在将来的JDK实现中,返回的可能不一定是FutureTask。
Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled- ThreadPoolExecutor执行。它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。
除了可以自己创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个
Runnable包装成一个Callable。
下面是Executors提供的,把一个Runnable包装成一个Callable的API。
public static Callable<Object> callable(Runnable task) // 假设返回对象Callable1
下面是Executors提供的,把一个Runnable和一个待返回的结果包装成一个Callable的API。
public static <T> Callable<T> callable(Runnable task, T result) // 假设返回对象Callable
前面讲过,当我们把一个Callable对象(比如上面的Callable1或Callable2)提交给ThreadPoolExecutor或ScheduledThreadPoolExecutor执行时,submit(…)会向我们返回一个FutureTask对象。我们可以执行FutureTask.get()方法来等待任务执行完成。当任务成功完成后FutureTask.get()将返回该任务的结果。例如,如果提交的是对象Callable1,FutureTask.get()方法将返回null;如果提交的是对象Callable2,FutureTask.get()方法将返回result对象。
线程池能够对线程进行统一分配,调优和监控:
Java是如何实现和管理线程池的?
从JDK 5开始,把工作单元与执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
程序中我们创建了固定大小为五个工作线程的线程池。然后分配给线程池十个工作,因为线程池大小为五,它将启动五个工作线程先处理五个工作,其他的工作则处于等待状态,一旦有工作完成,空闲下来工作线程就会捡取等待队列里的其他工作进行执行。
这里是以上程序的输出。
输出表明线程池中至始至终只有五个名为 "pool-1-thread-1" 到 "pool-1-thread-5" 的五个线程,这五个线程不随着工作的完成而消亡,会一直存在,并负责执行分配给线程池的任务,直到线程池消亡。
Executors 类提供了使用了 ThreadPoolExecutor 的简单的 ExecutorService 实现,但是 ThreadPoolExecutor 提供的功能远不止于此。我们可以在创建 ThreadPoolExecutor 实例时指定活动线程的数量,我们也可以限制线程池的大小并且创建我们自己的 RejectedExecutionHandler 实现来处理不能适应工作队列的工作。
这里是我们自定义的 RejectedExecutionHandler 接口的实现。
ThreadPoolExecutor 提供了一些方法,我们可以使用这些方法来查询 executor 的当前状态,线程池大小,活动线程数量以及任务数量。因此我是用来一个监控线程在特定的时间间隔内打印 executor 信息。
这里是使用 ThreadPoolExecutor 的线程池实现例子。
注意在初始化 ThreadPoolExecutor 时,我们保持初始池大小为 2,最大池大小为 4 而工作队列大小为 2。因此如果已经有四个正在执行的任务而此时分配来更多任务的话,工作队列将仅仅保留他们(新任务)中的两个,其他的将会被 RejectedExecutionHandlerImpl 处理。
上面程序的输出可以证实以上观点。
注意 executor 的活动任务、完成任务以及所有完成任务,这些数量上的变化。我们可以调用 shutdown() 方法来结束所有提交的任务并终止线程池。
其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
当一个任务提交至线程池之后:
当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.
LinkedBlockingQueue比ArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
线程池的线程数量达corePoolSize后,即使线程池没有可执行任务时,也不会释放线程。
FixedThreadPool的工作队列为队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:
初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.
由于使用了队列, 所以SingleThreadPool永远不会拒绝, 即饱和策略失效
线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列; 和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 执行过程与前两种稍微不同:
遍历线程池中的所有线程,然后逐个调用线程的interrupt方法来中断线程.
将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程.
将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程. 只要调用这两个关闭方法中的任意一个, isShutDown() 返回true. 当所有任务都成功关闭了, isTerminated()返回true.
其中AtomicInteger变量ctl的功能非常强大: 利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:
execute –> addWorker –>runworker (getTask)
线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。 从Woker类的构造方法实现可以发现: 线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。 firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
ThreadPoolExecutor.execute(task)实现了Executor.execute(task)
在多线程环境下,线程池的状态时刻在变化,而ctl.get()是非原子操作,很有可能刚获取了线程池状态后线程池状态就改变了。判断是否将command加入workque是线程池之前的状态。倘若没有double check,万一线程池处于非running状态(在多线程环境下很有可能发生),那么command永远不会执行。
从方法execute的实现可以看出: addWorker主要负责创建新的线程并执行任务 线程池创建新线程执行任务时,需要 获取全局锁:
一些属性还有构造方法:
runWorker方法是线程池的核心:
通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
下面来看一下getTask()方法,这里面涉及到keepAliveTime的使用,从这个方法我们可以看出线程池是怎么让超过corePoolSize的那部分worker销毁的。
注意这里一段代码是keepAliveTime起作用的关键:
allowCoreThreadTimeOut为false,线程即使空闲也不会被销毁;倘若为ture,在keepAliveTime内仍空闲则会被销毁。
如果线程允许空闲等待而不被销毁timed == false,workQueue.take任务: 如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行;
如果线程不允许无休止空闲timed == true, workQueue.poll任务: 如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;
在实际业务场景中,Future和Callable基本是成对出现的,Callable负责产生结果,Future负责获取结果。
AbstractExecutorService.submit()实现了ExecutorService.submit() 可以获取执行完的返回值, 而ThreadPoolExecutor 是AbstractExecutorService.submit()的子类,所以submit方法也是ThreadPoolExecutor`的方法。
通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。通过Executor.execute方法提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法;
public class FutureTask<V> implements RunnableFuture<V> 可以将FutureTask提交至线程池中等待被执行(通过FutureTask的run方法来执行)
内部状态的修改通过sun.misc.Unsafe修改
内部通过awaitDone方法对主线程进行阻塞,具体实现如下:
run方法
FutureTask.run方法是在线程池中被执行的,而非主线程
shutdown方法会将线程池的状态设置为SHUTDOWN,线程池进入这个状态后,就拒绝再接受任务,然后会将剩余的任务全部执行完
shutdownNow做的比较绝,它先将线程池状态设置为STOP,然后拒绝所有提交的任务。最后中断左右正在运行中的worker,然后清空任务队列。
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
首先引入:commons-lang3包
首先引入:com.google.guava包
spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:
可以使用ThreadPoolExecutor以下方法:
带着BAT大厂的面试问题去理解ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor,为任务提供延迟或周期执行,属于线程池的一种。和 ThreadPoolExecutor 相比,它还具有以下几种特性:
3.2 ScheduledThreadPoolExecutor数据结构ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor:
ScheduledThreadPoolExecutor 内部构造了两个内部类 ScheduledFutureTask 和 DelayedWorkQueue:
以下源码的解析是基于你已经理解了FutureTask。
说明: ScheduledFutureTask 的run方法重写了 FutureTask 的版本,以便执行周期任务时重置/重排序任务。任务的执行通过父类 FutureTask 的run实现。内部有两个针对周期任务的方法:
reExecutePeriodic与delayedExecute的执行策略一致,只不过reExecutePeriodic不会执行拒绝策略而是直接丢掉任务。
ScheduledFutureTask.cancel本质上由其父类 FutureTask.cancel 实现。取消任务成功后会根据removeOnCancel参数决定是否从队列中移除此任务。
首先看下构造函数,ScheduledThreadPoolExecutor 内部有四个构造函数,这里我们只看这个最大构造灵活度的:
构造函数都是通过super调用了ThreadPoolExecutor的构造,并且使用特定等待队列DelayedWorkQueue。
说明: schedule主要用于执行一次性(延迟)任务。函数执行逻辑分两步:
说明: delayedExecute是执行任务的主方法,方法执行逻辑如下:
A: 如果池正在运行,或者 run-after-shutdown 参数值为true,则调用父类方法ensurePrestart启动一个新的线程等待执行任务。ensurePrestart源码如下:
ensurePrestart是父类 ThreadPoolExecutor 的方法,用于启动一个新的工作线程等待执行任务,即使corePoolSize为0也会安排一个新线程。
B: 如果池已经关闭,并且 run-after-shutdown 参数值为false,则执行父类(ThreadPoolExecutor)方法remove移除队列中的指定任务,成功移除后调用ScheduledFutureTask.cancel取消任务
说明: scheduleAtFixedRate和scheduleWithFixedDelay方法的逻辑与schedule类似。
注意scheduleAtFixedRate和scheduleWithFixedDelay的区别: 乍一看两个方法一模一样,其实,在unit.toNanos这一行代码中还是有区别的。没错,scheduleAtFixedRate传的是正值,而scheduleWithFixedDelay传的则是负值,这个值就是 ScheduledFutureTask 的period属性。
说明: 池关闭方法调用了父类ThreadPoolExecutor的shutdown,具体分析见 ThreadPoolExecutor 篇。这里主要介绍以下在shutdown方法中调用的关闭钩子onShutdown方法,它的主要作用是在关闭线程池后取消并清除由于关闭策略不应该运行的所有任务,这里主要是根据 run-after-shutdown 参数(continueExistingPeriodicTasksAfterShutdown和executeExistingDelayedTasksAfterShutdown)来决定线程池关闭后是否关闭已经存在的任务。
例如: 由于 ScheduledThreadPoolExecutor 是一个固定核心线程数大小的线程池,并且使用了一个队列,所以调整maximumPoolSize对其没有任何影响(所以 ScheduledThreadPoolExecutor 没有提供可以调整最大线程数的构造函数,默认最大线程数固定为Integer.MAX_VALUE)。此外,设置corePoolSize为0或者设置核心线程空闲后清除(allowCoreThreadTimeOut)同样也不是一个好的策略,因为一旦周期任务到达某一次运行周期时,可能导致线程池内没有线程去处理这些任务。
注意: newScheduledThreadPool(1, threadFactory) 不等价于newSingleThreadScheduledExecutor。newSingleThreadScheduledExecutor创建的线程池保证内部只有一个线程执行任务,并且线程数不可扩展;而通过newScheduledThreadPool(1, threadFactory)创建的线程池可以通过setCorePoolSize方法来修改核心线程数。
带着BAT大厂的面试问题去理解FutureTask
FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。
可以看到,FutureTask实现了RunnableFuture接口,则RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能当做一个Runnable直接被Thread执行,也能作为Future用来得到Callable的计算结果。
Callable是个泛型接口,泛型V就是要call()方法返回的类型。对比Runnable接口,Runnable不会返回数据也不能抛出异常。
Future接口代表异步计算的结果,通过Future接口提供的方法可以查看异步计算是否执行完成,或者等待执行结果并获取执行结果,同时还可以取消执行。Future接口的定义如下:
其中需要注意的是state是volatile类型的,也就是说只要有任何一个线程修改了这个变量,那么其他所有的线程都会知道最新的值。7种状态具体表示:
各个状态之间的可能转换关系如下图所示:
这个构造函数会把传入的Callable变量保存在this.callable字段中,该字段定义为private Callable<V> callable;用来保存底层的调用,在被执行完成以后会指向null,接着会初始化state字段为NEW。
这个构造函数会把传入的Runnable封装成一个Callable对象保存在callable字段中,同时如果任务执行成功的话就会返回传入的result。这种情况下如果不需要返回值的话可以传入一个null。
顺带看下Executors.callable()这个方法,这个方法的功能是把Runnable转换成Callable,代码如下:
可以看到这里采用的是适配器模式,调用RunnableAdapter<T>(task, result)方法来适配,实现如下:
这个适配器很简单,就是简单的实现了Callable接口,在call()实现中调用Runnable.run()方法,然后把传入的result作为任务的结果返回。
在new了一个FutureTask对象之后,接下来就是在另一个线程中执行这个Task,无论是通过直接new一个Thread还是通过线程池,执行的都是run()方法,接下来就看看run()方法的实现。
说明:
说明:FutureTask 通过get()方法获取任务执行结果。如果任务处于未完成的状态(state <= COMPLETING),就调用awaitDone方法(后面单独讲解)等待任务完成。任务完成后,通过report方法获取执行结果或抛出执行期间的异常。report源码如下:
说明:awaitDone用于等待任务完成,或任务因为中断或超时而终止。返回任务的完成状态。函数执行逻辑如下:
如果线程被中断,首先清除中断状态,调用removeWaiter移除等待节点,然后抛出InterruptedException。removeWaiter源码如下:
说明:尝试取消任务。如果任务已经完成或已经被取消,此操作会失败。
常用使用方式: