一个java程序启动后至少有几个线程?他们的作用是什么?_java面试技能树9—并发...

1.理论

1.1. 线程

线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)。java.lang.Thread对象负责统计和控制这种行为。

每个程序都至少拥有一个线程-即作为Java虚拟机(JVM)启动参数运行在主类main方法的线程。在Java虚拟机初始化过程中也可能启动其他的后台线程。这种线程的数目和种类因JVM的实现而异。然而所有用户级线程都是显式被构造并在主线程或者是其他用户线程中被启动。

线程是操作系统调度的最小单元,在一个进程中可以创建多个线程,这些线程都有各自的计算器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

public class MultiThread {
    public static void main(String[] args) {
        // 获取Java线程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程线程,仅打印线程ID和线程名称信息
        for(ThreadInfo threadInfo : threadInfos) {
            System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
        }
    }
}

可以看出Java程序本身就是多线程,运行结果如下

[5]Attach Listener
[4]Signal Dispatcher
[3]Finalizer
[2]Reference Handler
[1]main

1.1.0 使用多线程的原因

1.1.0.1 更多的处理器核心

随着处理器上的核心数量越来越多,以及超线程技术的广泛运用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式,也从更高的主频向更多的核心发展。

1.1.0.2 更快的响应时间

有时我们会编写一些业务逻辑比较复杂的代码,例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快地完成呢?

在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验。

1.1.0.3更好的编程模型

Java为多线程编程提供了一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,而不是绞尽脑汁地考虑如何将其多线程化。

1.1.1 构造方法

Thread类中不同的构造方法接受如下参数的不同组合: - 一个Runnable对象,这种情况下,Thread.start方法将会调用对应Runnable对象的run方法。如果没有提供Runnable对象,那么就会立即得到一个Thread.run的默认实现。 - 一个作为线程标识名的String字符串,该标识在跟踪和调试过程中会非常有用,除此别无它用。 - 线程组(ThreadGroup),用来放置新创建的线程,如果提供的ThreadGroup不允许被访问,那么就会抛出一个SecurityException 。

Thread类本身就已经实现了Runnable接口,因此,除了提供一个用于执行的Runnable对象作为构造参数的办法之外,也可以创建一个Thread的子类,通过重写其run方法来达到同样的效果。然而,比较好的实践方法却是分开定义一个Runnable对象并用来作为构造方法的参数。将代码分散在不同的类中使得开发人员无需纠结于Runnable和Thread对象中使用的同步方法或同步块之间的内部交互。更普遍的是,这种分隔使得对操作的本身与其运行的上下文有着独立的控制。更好的是,同一个Runnable对象可以同时用来初始化其他的线程,也可以用于构造一些轻量化的执行框架(Executors)。另外需要提到的是通过继承Thread类实现线程的方式有一个缺点:使得该类无法再继承其他的类。

Thread对象拥有一个守护(daemon)标识属性,这个属性无法在构造方法中被赋值,但是可以在线程启动之前设置该属性(通过setDaemon方法)。当程序中所有的非守护线程都已经终止,调用setDaemon方法可能会导致虚拟机粗暴的终止线程并退出。isDaemon方法能够返回该属性的值。守护状态的作用非常有限,即使是后台线程在程序退出的时候也经常需要做一些清理工作。(daemon的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程,比如打印机队列管理,它总是在系统中运行。)

1.1.2 启动线程

调用start方法会触发Thread实例以一个新的线程启动其run方法。新线程不会持有调用线程的任何同步锁。

当一个线程正常地运行结束或者抛出某种未检测的异常(比如,运行时异常(RuntimeException),错误(ERROR) 或者其子类)线程就会终止。当线程终止之后,是不能被重新启动的。在同一个Thread上调用多次start方法会抛出InvalidThreadStateException异常。

如果线程已经启动但是还没有终止,那么调用isAlive方法就会返回true.即使线程由于某些原因处于阻塞(Blocked)状态该方法依然返回true。如果线程已经被取消(cancelled),那么调用其isAlive在什么时候返回false就因各Java虚拟机的实现而异了。没有方法可以得知一个处于非活动状态的线程是否已经被启动过了(译者注:即线程在开始运行前和结束运行后都会返回false,你无法得知处于false的线程具体的状态)。另一点,虽然一个线程能够得知同一个线程组的其他线程的标识,但是却无法得知自己是由哪个线程调用启动的。

1.1.3 优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

Java虚拟机为了实现跨平台(不同的硬件平台和各种操作系统)的特性,Java语言在线程调度与调度公平性上未作出任何的承诺,甚至都不会严格保证线程会被执行。但是Java线程却支持优先级的方法,这些方法会影响线程的调度: - 每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10) - 默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5). - 线程的当前优先级可以通过getPriority方法获得。 - 线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。 - 设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级 - 偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占

当可运行的线程数超过了可用的CPU数目的时候,线程调度器更偏向于去执行那些拥有更高优先级的线程。具体的策略因平台而异。比如有些Java虚拟机实现总是选择当前优先级最高的线程执行。有些虚拟机实现将Java中的十个优先级映射到系统所支持的更小范围的优先级上,因此,拥有不同优先级的线程可能最终被同等对待。还有些虚拟机会使用老化策略(随着时间的增长,线程的优先级逐渐升高)动态调整线程优先级,另一些虚拟机实现的调度策略会确保低优先级的线程最终还是能够有机会运行。设置线程优先级可以影响在同一台机器上运行的程序之间的调度结果,但是这不是必须的。

线程优先级对语义和正确性没有任何的影响。特别是,优先级管理不能用来代替锁机制。优先级仅仅是用来表明哪些线程是重要紧急的,当存在很多线程在激励进行CPU资源竞争的情况下,线程的优先级标识将会显得非常有用。比如,在ParticleApplet中将particle animation线程的优先级设置的比创建它们的applet线程低,在某些系统上能够提高对鼠标点击的响应,而且不会对其他功能造成影响。但是即使setPriority方法被定义为空实现,程序在设计上也应该保证能够正确执行(尽管可能会没有响应)。

下面这个表格列出不同类型任务在线程优先级设定上的通常约定。在很多并发应用中,在任一指定的时间点上,只有相对较少的线程处于可执行的状态(另外的线程可能由于各种原因处于阻塞状态),在这种情况下,没有什么理由需要去管理线程的优先级。另一些情况下,在线程优先级上的调整可能会对并发系统的调优起到一些作用。

范围  用途
10      Crisis management(应急处理)
7-9    Interactive, event-driven(交互相关,事件驱动)
4-6    IO-bound(IO限制类)
2-3    Background computation(后台计算)
1        Run only if nothing else can(仅在没有任何线程运行时运行的)

1.1.4 控制方法

只有很少几个方法可以用于跨线程交流: - 每个线程都有一个相关的Boolean类型的中断标识。在线程t上调用t.interrupt会将该线程的中断标识设为true,除非线程t正处于Object.wait,Thread.sleep,或者Thread.join,这些情况下interrupt调用会导致t上的这些操作抛出InterruptedException异常,但是t的中断标识会被设为false。 - 任何一个线程的中断状态都可以通过调用isInterrupted方法来得到。如果线程已经通过interrupt方法被中断,这个方法将会返回true。 - 但是如果调用了Thread.interrupted方法且中断标识还没有被重置,或者是线程处于wait,sleep,join过程中,调用isInterrupted方法将会抛出InterruptedException异常。调用t.join()方法将会暂停执行调用线程,直到线程t执行完毕:当t.isAlive()方法返回false的时候调用t.join()将会直接返回(return)。另一个带参数毫秒(millisecond)的join方法在被调用时,如果线程没能够在指定的时间内完成,调用线程将重新得到控制权。因为isAlive方法的实现原理,所以在一个还没有启动的线程上调用join方法是没有任何意义的。同样的,试图在一个还没有创建的线程上调用join方法也是不明智的。

起初,Thread类还支持一些另外一些控制方法:suspend,resume,stop以及destroy。这几个方法已经被声明过期。其中destroy方法从来没有被实现,估计以后也不会。而通过使用等待/唤醒机制增加suspend和resume方法在安全性和可靠性的效果有所欠缺。

1.1.5 静态方法

Thread类中的部分方法被设计为只适用于当前正在运行的线程(即调用Thread方法的线程)。为强调这点,这些方法都被声明为静态的。 - Thread.currentThread方法会返回当前线程的引用,得到这个引用可以用来调用其他的非静态方法,比如Thread.currentThread().getPriority()会返回调用线程的优先级。 - Thread.interrupted方法会清除当前线程的中断状态并返回前一个状态。(一个线程的中断状态是不允许被其他线程清除的) - Thread.sleep(long msecs)方法会使得当前线程暂停执行至少msecs毫秒。

Thread.yield方法纯粹只是建议Java虚拟机对其他已经处于就绪状态的线程(如果有的话)调度执行,而不是当前线程。最终Java虚拟机如何去实现这种行为就完全看其喜好了。

尽管缺乏保障,但在不支持分时间片/可抢占式的线程调度方式的单CPU的Java虚拟机实现上,yield方法依然能够起到切实的作用。在这种情况下,线程只在被阻塞的情况下(比如等待IO,或是调用了sleep等)才会进行重新调度。在这些系统上,那些执行非阻塞的耗时的计算任务的线程就会占用CPU很长的时间,最终导致应用的响应能力降低。如果一个非阻塞的耗时计算线程会导致时间处理线程或者其他交互线程超出可容忍的限度的话,就可以在其中插入yield操作(或者是sleep),使得具有较低线程优先级的线程也可以执行。为了避免不必要的影响,你可以只在偶然间调用yield方法,比如,可以在一个循环中插入如下代码:if (Math.random() < 0.01) Thread.yield();

在支持可抢占式调度的Java虚拟机实现上,线程调度器忽略yield操作可能是最完美的策略,特别是在多核处理器上。

1.1.6 线程组

每一个线程都是一个线程组中的成员。默认情况下,新建线程和创建它的线程属于同一个线程组。线程组是以树状分布的。当创建一个新的线程组,这个线程组成为当前线程组的子组。getThreadGroup方法会返回当前线程所属的线程组,对应地,ThreadGroup类也有方法可以得到哪些线程目前属于这个线程组,比如enumerate方法。

ThreadGroup类存在的一个目的是支持安全策略来动态的限制对该组的线程操作。比如对不属于同一组的线程调用interrupt是不合法的。这是为避免某些问题(比如,一个applet线程尝试杀掉主屏幕的刷新线程)所采取的措施。ThreadGroup也可以为该组所有线程设置一个最大的线程优先级。

线程组往往不会直接在程序中被使用。在大多数的应用中,如果仅仅是为在程序中跟踪线程对象的分组,那么普通的集合类(比如java.util.Vector)应是更好的选择。

在ThreadGroup类为数不多的几个方法中,uncaughtException方法却是非常有用的,当线程组中的某个线程因抛出未检测的异常(比如空指针异常NullPointerException)而中断的时候,调用这个方法可以打印出线程的调用栈信息。

1.1.7 线程的状态

Java线程在运行的生命周期中可能处于下表所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。 - new:初始状态,线程被创建,但是还没有调用start()方法 - runnable:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” - blocked:阻塞状态,表示线程阻塞于锁 - waiting:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) - time_waiting:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 - terminated:终止状态,表示当前线程已经执行完毕

线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下图:

e652f15a3d8bf1fc5a9c4a3bd9c54fd0.png


Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

1.2 同步

1.2.1 对象与锁

每一个Object类及其子类的实例都拥有一个锁。其中,标量类型int,float等不是对象类型,但是标量类型可以通过其包装类来作为锁。单独的成员变量是不能被标明为同步的。锁只能用在使用了这些变量的方法上。然而正如在2.2.7.4上描述的,成员变量可以被声明为volatile,这种方式会影响该变量的原子性,可见性以及排序性。

类似的,持有标量变量元素的数组对象拥有锁,但是其中的标量元素却不拥有锁。(也就是说,没有办法将数组成员声明为volatile类型的)。如果锁住了一个数组并不代表其数组成员都可以被原子的锁定。也没有能在一个原子操作中锁住多个对象的方法。

Class实例本质上是个对象。正如下所述,在静态同步方法中用的就是类对象的锁。

1.2.2 同步方法和同步块

使用synchronized关键字,有两种语法结构:同步代码块和同步方法。同步代码块需要提供一个作为锁的对象参数。这就允许了任意方法可以去锁任一一个对象。但在同步代码块中使用的最普通的参数却是this。

同步代码块被认为比同步方法更加的基础。如下两种声明方式是等同的:

synchronized void f() { /* body */ }

void f() { synchronized(this) { /* body */ } }

synchronized关键字并不是方法签名的一部分。所以当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,另外,构造方法不可能是真正同步的(尽管可以在构造方法中使用同步块)。

同步实例方法在其子类和父类中使用同样的锁。但是内部类方法的同步却独立于其外部类, 然而一个非静态的内部类方法可以通过下面这种方式锁住其外部类:

synchronized(OuterClass.this) { /* body */ }

1.2.3 等待锁与释放锁

使用synchronized关键字须遵循一套内置的锁等待-释放机制。所有的锁都是块结构的。当进入一个同步方法或同步块的时候必须获得该锁,而退出的时候(即使是异常退出)必须释放这个锁。你不能忘记释放锁。

锁操作是建立在独立的线程上的而不是独立的调用基础上。一个线程能够进入一个同步代码的条件是当前锁未被占用或者是当前线程已经占用了这个锁,否则线程就会阻塞住。(这种可重入锁或是递归锁不同于POSIX线程)。这就允许一个同步方法可以去直接调用同一个锁管理的另一个同步方法,而不需要被冻结(注:即不需要再经历释放锁-阻塞-申请锁的过程)。

同步方法或同步块遵循这种锁获取/锁释放的机制有一个前提,那就是所有的同步方法或同步块都是在同一个锁对象上。如果一个同步方法正在执行中,其他的非同步方法也可以在任何时候执行。也就是说,同步不等于原子性,但是同步机制可以用来实现原子性。

当一个线程释放锁的时候,另一个线程可能正等待这个锁(也可能是同一个线程,因为这个线程可能需要进入另一个同步方法)。但是关于哪一个线程能够紧接着获得这个锁以及什么时候,这是没有任何保证的。另外,没有什么办法能够得到一个给定的锁正被哪个线程拥有着。

除了锁控制之外,同步也会对底层的内存系统带来副作用。

1.2.4 静态变量/方法

锁住一个对象并不会原子性的保护该对象类或其父类的静态成员变量。而应该通过同步的静态方法或代码块来保证访问一个静态的成员变量。静态同步使用的是静态方法锁声明的类对象所拥有的锁。类C的静态锁可以通过内置的实例方法获取到:

synchronized(C.class) { /* body */ }

每个类所对应的静态锁和其他的类(包括其父类)没有任何的关系。通过在子类中增加一个静态同步方法来试图保护父类中的静态成员变量是无效的。应使用显式的代码块来代替。

如下这种方式也是一种不好的实践:

synchronized(getClass()) { /* body */ } // Do not use

这种方式,可能锁住的实际中的类,并不是需要保护的静态成员变量所对应的类(有可能是其子类)

Java虚拟机在类加载和类初始化阶段,内部获得并释放类锁。除非你要去写一个特殊的类加载器或者需要使用多个锁来控制静态初始顺序,这些内部机制不应该干扰普通类对象的同步方法和同步块的使用。Java虚拟机没有什么内部操作可以独立的获取你创建和使用的类对象的锁。然而当你继承java.*的类的时候,你需要特别小心这些类中使用的锁机制。

1.3 监视器

正如每个对象都有一个锁一样,每一个对象同时拥有一个由这些方法(wait,notify,notifyAll,Thread,interrupt)管理的一个等待集合。拥有锁和等待集合的实体通常被称为监视器(虽然每种语言定义的细节略有不同),任何一个对象都可以作为一个监视器。

对象的等待集合是由Java虚拟机来管理的。每个等待集合上都持有在当前对象上等待但尚未被唤醒或是释放的阻塞线程。

因为与等待集合交互的方法(wait,notify,notifyAll)只在拥有目标对象的锁的情况下才被调用,因此无法在编译阶段验证其正确性,但在运行阶段错误的操作会导致抛出IllegalMonitorStateException异常。

1.3.1 Wait

调用wait方法会产生如下操作: - 如果当前线程已经终止,那么这个方法会立即退出并抛出一个InterruptedException异常。否则当前线程就进入阻塞状态。 - Java虚拟机将该线程放置在目标对象的等待集合中。 - 释放目标对象的同步锁,但是除此之外的其他锁依然由该线程持有。即使是在目标对象上多次嵌套的同步调用,所持有的可重入锁也会完整的释放。这样,后面恢复的时候,当前的锁状态能够完全地恢复。

1.3.2 Notify

调用Notify会产生如下操作: - Java虚拟机从目标对象的等待集合中随意选择一个线程(称为T,前提是等待集合中还存在一个或多个线程)并从等待集合中移出T。当等待集合中存在多个线程时,并没有机制保证哪个线程会被选择到。 - 线程T必须重新获得目标对象的锁,直到有线程调用notify释放该锁,否则线程会一直阻塞下去。如果其他线程先一步获得了该锁,那么线程T将继续进入阻塞状态。 - 线程T从之前wait的点开始继续执行。

1.3.3 NotifyAll

notifyAll方法与notify方法的运行机制是一样的,只是这些过程是在对象等待集合中的所有线程上发生(事实上,是同时发生)的。但是因为这些线程都需要获得同一个锁,最终也只能有一个线程继续执行下去。

1.3.4 Interrupt(中断)

如果在一个因wait而中断的线程上调用Thread.interrupt方法,之后的处理机制和notify机制相同,只是在重新获取这个锁之后,该方法将会抛出一个InterruptedException异常并且线程的中断标识将被设为false。如果interrupt操作和一个notify操作在同一时间发生,那么不能保证那个操作先被执行,因此任何一个结果都是可能的。(JLS的未来版本可能会对这些操作结果提供确定性保证)

1.3.5 Timed Wait(定时等待)

定时版本的wait方法,wait(long mesecs)和wait(long msecs,int nanosecs),参数指定了需要在等待集合中等待的最大时间值。如果在时间限制之内没有被唤醒,它将自动释放,除此之外,其他的操作都和无参数的wait方法一样。并没有状态能够表明线程正常唤醒与超时唤醒之间的不同。需要注意的是,wait(0)与wait(0,0)方法其实都具有特殊的意义,其相当于不限时的wait()方法,这可能与你的直觉相反。

由于线程竞争,调度策略以及定时器粒度等方面的原因,定时等待方法可能会消耗任意的时间。(注:关于定时器粒度并没有任何的保证,目前大多数的Java虚拟机实现当参数设置小于1毫秒的时候,观察的结果基本上在1~20毫秒之间)

Thread.sleep(long msecs)方法使用了定时等待的wait方法,但是使用的并不是当前对象的同步锁。它的效果如下描述:

if (msecs != 0)  {
    Object s = new Object();
    synchronized(s) { 
        s.wait(msecs); 
    }
}

当然,系统不需要使用这种方式去实现sleep方法。需要注意的,sleep(0)方法的含义是中断线程至少零时间,随便怎么解释都行。(译者注:该方法有着特殊的作用,从原理上它可以促使系统重新进行一次CPU竞争)。

1.4 Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。

在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。如下代码:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DeamonRunner(),"DeamonRunner");
        thread.setDaemon(true); 
        thread.start(); 
    } 

    static class DeamonRunner implements Runnable{
        @Override 
        public void run() {
            try {
                Thread.sleep(2000l); 
            } catch (InterruptedException e) {
                //
            }finally {
                System.out.println("DeamonThread finally run."); 
            } 
        } 
    } 
}

运行Deamon程序,可以看到在终端或者命令提示符没有任何输出。

1.5 启动线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。

private void init(ThreadGroup g, Runnable target, String name,long stackSize,AccessControlContext acc) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    } 
    // 当前线程就是该线程的父线程 
    Thread parent = currentThread(); 
    this.group = g; 
    // 将daemon、priority属性设置为父线程的对应属性 
    this.daemon = parent.isDaemon(); 
    this.priority = parent.getPriority(); 
    this.name = name.toCharArray(); 
    this.target = target; 
    setPriority(priority); 
    // 将父线程的InheritableThreadLocal复制过来 
    if (parent.inheritableThreadLocals != null){
        this.inheritableThreadLocals=ThreadLocal .createInheritedMap(parent.inheritableThreadLocals); 
    }
    // 分配一个线程ID 
    tid = nextThreadID(); 
}

在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Deamon、优先级和加载资源的ContextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

1.6 理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(longmillis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

1.7 过期的suspend()、resume()和stop()

suspend()、resume()和stop()方法完成了线程的暂停、恢复和终止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用等待/通知机制来替代。

1.8 安全地终止线程

中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。

public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false;
        }
    }
}

main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

1.9 线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

1.10 volatile和synchronized关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

通过使用javap工具查看生成的class文件信息来分析synchronized关键字的实现细节,代码如下

public class Synchronized {
    public static void main(String[] args) {
        synchronized (Synchronized.class){
            m();
        }
    }

    public static synchronized void m(){

    }
}

对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依赖方法修饰符上的ACC_SYNCHRONIZED来完成。无论采用哪种方式,其本质是对一个对象的监视器进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

1.11 等待/通知机制

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方通知方之间的交互工作。

等待/通知的相关方法是任意Java对象都具备的,这些方法被定义在所有对象的超类java.lang.Object上。

1.12 实现案例

1.12.1 生产者-消费者模型

public class WaitNotify {

    private final static int CONTAINER_MAX_LENGTH = 3;
    private static Queue<Integer> resources = new LinkedList<Integer>();

    //作为synchronized的对象监视器
    private static final Object lock = new Object();

    /**
     * 消费者
     * @param args
     */
    static class Consumer implements Runnable{
        @Override
        public void run() {
            synchronized(lock) {
                // 不能使用if判断,防止过早唤醒
                while(resources.isEmpty()) {
                    try {
                        // 当前释放锁,线程进入等待状态
                        lock.wait();
                    } catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "get number is" + resources.remove());
                // 唤醒所有等待状态的线程
                lock.notifyAll();
            }
        }
    }
    /**
     * 生产者
     * @author Administrator
     *
     */
    static class Producer implements Runnable{
        @Override
        public void run() {
            synchronized(lock) {
                while(resources.size() == CONTAINER_MAX_LENGTH) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int number = (int)(Math.random() * 100);
                System.out.println(Thread.currentThread().getName() + "produce number is" + number);
                resources.add(number);
                lock.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        for(int i = 0; i < 50; i++) {
            new Thread(new Consumer(), "consumer-" + i).start();
        }

        for(int i = 0; i < 50; i++) {
            new Thread(new Producer(), "producer-" + i).start();
        }
    }

}

调用wait()/notify()/notifyAll()需要注意的细节: - 使用wait()/notify()/notifyAll()需要先调用对象加锁; - 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列; - notify()/notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()/notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回; - notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。 - 从wait()方法返回的前提是获得了调用对象的锁。

1.12.2 设三个计线程,实现打印ABCABC...

public class ABCPrinter {

    private final static Object lock = new Object();

    static class Print implements Runnable{
        private int max_print;
        private int count = 0;
        private String str = "A";

        public Print(int max_print) {
            this.max_print = max_print;
        }

        @Override
        public void run() {
            synchronized(lock) {
                String name = Thread.currentThread().getName();
                while(count < max_print) {
                    if(str.equals(name)) {
                        System.out.print(name);
                        if(str.equals("A")) {
                            str = "B";
                        } else if(str.equals("B")) {
                            str = "C";
                        } else {
                            count++;
                            str = "A";
                        }
                        lock.notifyAll();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

    }

    public static void main(String[] args) {
        Print print = new Print(15);
        new Thread(print, "A").start();
        new Thread(print, "B").start();
        new Thread(print, "C").start();
    }
}

2.Excutors

2.1 Callable和Future

创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。

Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

2.1.1 介绍

Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并发的(并发是同一时间段多任务,并行是同一时刻多任务),我们必须等待它返回的结果。

java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。

2.1.2 Callable 和Runnable

Runnable接口只声明了一个方法:run()

public interface Runnable{
    public abstract void run();
}

由于run()方法返回值类型为void,因此执行完任务后不会返回任何结果。。

2.1.3 Callable

2.1.3.1 Callable接口

Callable接口声明了call()方法

public interface Callable<V>{
    /**
    * Computes a result, or throws an exception if unable to do so.
    *
    * @return computed result
    * @throws Exception if unable to compute a result
    */
    V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。Callable接口可以看作是Runnable接口的补充,call方法带有返回值,并且可以抛出异常。

2.1.3.2 Callable接口使用

一般情况下是配合ExecutorService来使用的

<T> Future<T> submit(Callable<T> task);

Future<?> submit(Runnable task);

2.1.4 Future

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

public interface Future<V>{
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit){
        throws InterruptedException, ExecutionException, TimeoutException;
    }
}

2.1.4.1 Future方法

2.1.4.1.1 cancel

cancel用于取消任务,如果取消成功返回true,取消失败,返回false.参数mayInterruptIfRunning表示是否取消正在执行却没有执行完的任务:

1)如果设置true,则表示可以取消正在执行过程中的任务。 - 如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false; - 如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true

2)若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。

2.1.4.1.2 isCancelled

isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。

2.1.4.1.3 isDone

isDone方法表示任务是否已经完成,若任务完成,则返回true;

2.1.4.1.4 get()

get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

2.1.4.1.5 get(long timeout, TimeUnit unit)

get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,直接抛出超时异常,InterruptedException。

2.1.4.2 Future功能

1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果;
因为Future只是一个接口,无法直接用来创建对象使用,因此使用FutureTask

2.1.5 FutureTask

FutureTask实现了Runnable和Future接口,定义如下

public interface RunnableFuture<V> extends Runnable, Future<V>{
    void run();
}

可以看到这个接口实现了Runnable和Future接口,接口中的具体实现由FutureTask来实现。这个类的两个构造方法如下 :

public FutureTask(Callable<V> callable){
    if(callable == null){
        throw new NullPointerException;
    }
    sync = new Sync(callable);
}

public FutureTask(Runnable runnable, V result){
    sync = new Sync(Executors.callable(runnable, result));
}

如上提供了两个构造函数,一个以Callable为参数,另外一个以Runnable为参数。这些类之间的关联对于任务建模的办法非常灵活,允许你基于FutureTask的Runnable特性(因为它实现了Runnable接口),把任务写成Callable,然后封装进一个由执行者调度并在必要时可以取消的FutureTask。

FutureTask可以由执行者调度,这一点很关键。它对外提供的方法基本上就是Future和Runnable接口的组合:get()、cancel、isDone()、isCancelled()和run(),而run()方法通常都是由执行者调用,我们基本上不需要直接调用它。

public class MyCallable implements Callable<String> {
    private long waitTime;
    public MyCallable(int timeInMillis) {
        this.waitTime = timeInMillis;
    }
    @Override
    public String call() throws Exception {
        Thread.sleep(waitTime);

        return Thread.currentThread().getName();
    }

}
public class FutureTaskExample {

    public static void main(String[] args) {
        // 要执行的任务  
        MyCallable callable1 = new MyCallable(1000);
        MyCallable callable2 = new MyCallable(2000);
        // 将Callable写的任务封装到一个由执行者调度的FutureTask对象  
        FutureTask<String> futureTask1 = new FutureTask<>(callable1);
        FutureTask<String> futureTask2 = new FutureTask<>(callable2);
        // 创建线程池并返回ExecutorService实例  
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // 执行任务 
        executor.execute(futureTask1);
        executor.execute(futureTask2);

        while(true) {
            try {
                if(futureTask1.isDone() && futureTask2.isDone()) {//  两个任务都完成
                    System.out.println("Done");
                    // 关闭线程池和服务
                    executor.shutdown();
                    return;
                }

                if(!futureTask1.isDone()) {// 任务1没有完成,会等待,直到任务完成
                    System.out.println("FutureTask output=" + futureTask1.get());
                }

                System.out.println("Waiting for FutureTask2 to complete");
                String s = futureTask2.get(200L, TimeUnit.MILLISECONDS);
                if(s != null) {
                    System.out.println("FutureTask2 output="+s);
                }
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            } catch(TimeoutException e) {

            }
        }
    }

}

输出结果为

FutureTask output=pool-1-thread-1
Waiting for FutureTask2 to complete
Waiting for FutureTask2 to complete
Waiting for FutureTask2 to complete
Waiting for FutureTask2 to complete
Waiting for FutureTask2 to complete
FutureTask2 output=pool-1-thread-2
Done

2.1.5.1 FutureTask状态

FutureTask中有一个表示任务状态的int值,初始为NEW。定义如下:

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;

可能的状态转换包括: - NEW -> COMPLETING -> NORMAL - NEW -> COMPLETING -> EXCEPTIONAL - NEW -> CANCELLED - NEW -> INTERRUPTING -> INTERRUPTED

2.1.5.2 构造方法

两种构造方法

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

 public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result); 
    this.state = NEW; // ensure visibility of callable 
 }

第二个方法是将Runnbale和结果组合成一个Callable,这个可以通过Excutors.callable()方法得出结论:

public satic <T> Callable<T> callable(Runnable task, T result){
    if(task == null){
        throw new NullPointerException();
    }
    return new RunnableAdapter<T>(task, result);
}

static final class RunnableAdapter<T> implements Callable<T>{
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result){
        this.task = task;
        this.result = result;
    }
    public T call(){
        tsk.run();
        return result;
    }
}

从上面可以看到RunnableAdapter实现了Callable并且在call方法中调用了Runnable的run方法,然后将结果返回,这其实就是一个适配器模式啊。

所以说两个构造方法最终都是得到了一个Callable以及设置了初始状态为NEW。

2.1.5.3 run()方法

当将FutureTask提交给Executor后,Executor执行FutureTask时会执行其run方法

public void run() {
        //如果状态不为NEW或者CAS当前执行线程失败,直接返回
        if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
            return;
        //尝试调用Callable.call
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    //出现异常了,调用setException方法
                    result = null;
                    ran = false;
                    setException(ex);
                }
                //如果成功了,调用set方法
                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);
        }
    }

从上面可以看到,任务可以被执行的前提是当前状态为NEW以及CAS当前执行线程成功,也就是runner值,代表执行Callable的线程。从这个看到run方法就是调用Callable的call方法,然后如果出现异常了就调用setException方法,如果成功执行了,那么调用set方法,下面我们分别来看这几种情况。

2.1.5.4 set方法

当Callable成功执行后,会调用set方法将结果传出

protected void set(V v) {
    //完成NEW->COMPLETING->NORMAL状态转换
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

从上面可以看到,将outcome变量赋值为结果,并将state状态更新,最后调用finishCompletion()方法。finishCompletion()方法将移除和通知所有等待线程。

2.1.5.5 setException方法

//完成NEW->COMPLETING->EXCEPTIONAL状态转换
protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

该方法和set方法类似,完成状态转换,将结果设置为Throwable并调用finishCompletion通知和移除等待线程。

2.1.5.6 get方法

当想得到FutureTask的结算结果时,调用get方法,get方法可以允许多个线程调用,下面的例子展示了多个线程调用get的情况。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("Start:" + System.nanoTime()); 
    FutureTask<Long> futureTask = new FutureTask<Long>(new SumTask()); 
    Executor executor=Executors.newSingleThreadExecutor(); 
    executor.execute(futureTask); 
    for(int i=0;i<5;i++){
        executor.execute(new Runnable() {
            @Override 
            public void run() {
                try {
                    System.out.println("get result "+futureTask.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        });
    } 
    System.out.println(futureTask.get()); 
    System.out.println("End:" + System.nanoTime()); 
}

该例子展示了一共有5个线程想得到FutureTask的结果,一旦调用get,那么该线程就会阻塞。FutureTask的get方法实现如下:

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

从上面的代码可以看到,如果当前任务的状态不大于COMPLETING,那么会调用awaitDone方法,这个方法会将调用的线程挂起;否则直接调用report方法返回结果。

在前面set和setException方法中可以得出结论:当状态从NEW变为COMPLETING后,才会将outcome赋值,也就是状态是NEW或者COMPLETING时,outcome都还未赋值,也就意味着计算仍在进行,那么此时想要get到结果,就必须等待。下面先看下awaitDone方法是如何将调用线程阻塞的。awaitDone的两个参数分别表示是否定时,以及定时的时间多少。get的另一个重载方法就提供了超时限制。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;
            //如果状态大于COMPLETING,说明已经计算已经完成了
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //状态是COMPLETING,在set和setException方法中可以看到处于该状态马上就会进入下一个状态
            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);
        }
    }

上面的代码中有一个WaitNode类,该类表示等待节点,保存等待的线程以及下一个节点,是一个单链表结构,其定义如下:

static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

awaitDone方法中进入死循环后,主要有几步: - 如果线程被中断了,移除节点,抛出异常 - 如果状态大于COMPLETING,那么直接返回 - 如果状态是COMPLETING,在set和setException可以看到,处于COMPLETING是一个暂时状态,很快就会进入下一个状态,所以这儿就调用了Thread.yield()方法让步一下 - 如果状态是NEW且节点为null,那么创建一个节点 - 如果还没有将当前线程加入队列,那么将当前线程加入到等待队列中。由于WaitNode是一个单链表结构,FutureTask中保存了waiters的变量,就可以沿着该变量得到所有等待的线程 - 如果限制了时间,那么计算出生出超出时间,挂起指定时间。当解除挂起时,如果计算还未完成,那么将会由于没有时间了,调用removeWaiter方法移除节点。 - 如果没有限制时间,那么将线程无限挂起

上面几种情况下,都涉及了移除节点,removeWaiter方法就是删除单链表中一个节点的实现。当线程被解除挂起,或计算已经完成后,将会get方法中将会调用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);
}

从上面可以看到report会根据任务的状态不同返回不同的结果。 - 如果计算正常结束,即状态是NORMAL,那么返回正确的计算结果 - 如果计算被取消了,即状态大于等于CANCELLED,那么抛出CancellationException - 如果计算以异常结束,即状态是EXCEPTIONAL,那么抛出ExecutionException

2.1.5.7 finishCompletion方法

在set方法和setException方法中,当将结果赋值后,都调用了finishCompletion方法来移除和通知等待线程。由于get方法中可以挂起了一群等待节点,那么当结果被计算出来了,自然应该通知那些等待线程。finishCompletion的实现如下:

private void finishCompletion() {
    //如果有等待线程,从头开始解除挂起
    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
}

finishCompletion的实现比较简单,就是遍历等待线程的单链表,释放那些等待线程。当线程被释放后,那么在awaitDone的死循环中就会进入下一个循环,由于状态已经变成了NORMAL或者EXCEPTIONAL,将会直接跳出循环。

释放了所有线程后,将会调用done()方法,FutureTask的done()方法默认没有任何实现,子类可以在该方法中调用完成回调以及记录操作等等。

2.1.5.8 cancel方法

cancel方法用于取消Callable的计算。参数mayInterruptIfRunning指明是否应该中断正在运行的任务,返回值表示取消是否成功了。其源码如下:

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try { 
        //如果需要中断
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally {
                //最终状态INTERRUPTED
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        //释放等待线程
        finishCompletion();
    }
    return true;
}

从上面可以看到如果是需要中断正在执行的任务,那么状态转换将会是NEW->INTERRPUTING->INTERRUPTED;如果不需要中断正在执行的任务,那么状态转换将会是NEW->CANCELD。不管是否中断,最终都会调用finishCompletion()完成对等待线程的释放。

当这些线程释放后,再进入到awaitDone中的循环时,返回的状态将会是大于等于CANCELD,在report方法中将会得到CancellationException异常。

2.1.5.9 isDone方法

Future接口中isDone方法表明任务是否已经完成了,如果完成了,那么返回true,否则false。下面是FutureTask的实现:

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

可以看到只要状态从初始状态NEW完成了一次转换,那么就说明任务已经被完成了。

2.1.6 总结

Callable是一种可以返回结果的任务,这是它与Runnable的区别,但是通过适配器模式可以使Runnable与Callable类似。Future代表了一个异步的计算,可以从中得到计算结果、查看计算状态,其实现FutureTask可以被提交给Executor执行,多个线程可以从中得到计算结果。Callable和Future是配合使用的,当从Future中get结果时,如果结果还没被计算出来,那么线程将会被挂起,FutureTak内部使用一个单链表维持等待的线程;当计算结果出来后,将会对等待线程解除挂起,等待线程就都可以得到计算结果了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值