点击蓝色“程序员cxuan ”关注我哟html
加个“星标”,欢迎来撩java

1程序员
并发历史 web
在计算机最先期的时候,没有操做系统,执行程序只须要一种方式,那就是从头至尾依次执行。任何资源都会为这个程序服务,在计算机使用某些资源时,其余资源就会空闲,就会存在 浪费资源
的状况。算法
❝这里说的浪费资源指的是资源空闲,没有充分使用的状况。数据库
❞
操做系统的出现为咱们的程序带来了 并发性
,操做系统使咱们的程序可以同时运行多个程序,一个程序就是一个进程,也就至关于同时运行多个进程。编程
操做系统是一个并发系统
,并发性是操做系统很是重要的特征,操做系统具备同时处理和调度多个程序的能力,好比多个 I/O 设备同时在输入输出;设备 I/O 和 CPU 计算同时进行;内存中同时有多个系统和用户程序被启动交替、穿插地执行。操做系统在协调和分配进程的同时,操做系统也会为不一样进程分配不一样的资源。数组
操做系统实现多个程序同时运行解决了单个程序没法作到的问题,主要有下面三点缓存
-
资源利用率
,咱们上面说到,单个进程存在资源浪费的状况,举个例子,当你在为某个文件夹赋予权限的时候,输入程序没法接受外部的输入字符,只有等到权限赋予完毕后才能接受外部输入。总的来说,就是在等待程序时没法执行其余工做。若是在等待程序时能够运行另外一个程序,那么将会大大提升资源的利用率。(资源并不会以为累)由于它不会划水~ -
公平性
,不一样的用户和程序都可以使用计算机上的资源。一种高效的运行方式是为不一样的程序划分时间片来使用资源,可是有一点须要注意,操做系统能够决定不一样进程的优先级。虽然每一个进程都有可以公平享有资源的权利,可是当有一个进程释放资源后的同时有一个优先级更高的进程抢夺资源,就会形成优先级低的进程没法得到资源,进而致使进程饥饿。 -
便利性
,单个进程是是不用通讯的,通讯的本质就是信息交换
,及时进行信息交换可以避免信息孤岛
,作重复性的工做;任何并发能作的事情,单进程也可以实现,只不过这种方式效率很低,它是一种顺序性
的。
可是,顺序编程(也称为串行编程
)也不是一无可取
的,串行编程的优点在于其「直观性和简单性」,客观来说,串行编程更适合咱们人脑的思考方式,可是咱们并不会知足于顺序编程,「we want it more!!!」 。资源利用率、公平性和便利性促使着进程出现的同时,也促使着线程
的出现。安全
若是你还不是很理解进程和线程的区别的话,那么我就以我多年操做系统的经验(吹牛逼,实则半年)来为你解释一下:「进程是一个应用程序,而线程是应用程序中的一条顺序流」。
进程中会有多个线程来完成一些任务,这些任务有可能相同有可能不一样。每一个线程都有本身的执行顺序。
每一个线程都有本身的栈空间,这是线程私有的,还有一些其余线程内部的和线程共享的资源,以下所示。
❝在计算机中,通常堆栈指的就是栈,而堆指的才是堆
❞
线程会共享进程范围内的资源,例如内存和文件句柄,可是每一个线程也有本身私有的内容,好比程序计数器、栈以及局部变量。下面汇总了进程和线程共享资源的区别
线程是一种轻量级
的进程,轻量级体如今线程的建立和销毁要比进程的开销小不少。
❝注意:任何比较都是相对的。
❞
在大多数现代操做系统中,都以线程为基本的调度单位,因此咱们的视角着重放在对线程
的探究。

2
线程
什么是多线程
多线程意味着你可以在同一个应用程序中运行多个线程,咱们知道,指令是在 CPU 中执行的,多线程应用程序就像是具备多个 CPU 在同时执行应用程序的代码。
其实这是一种假象,线程数量并不等于 CPU 数量,单个 CPU 将在多个线程之间共享 CPU 的时间片,在给定的时间片内执行每一个线程之间的切换,每一个线程也能够由不一样的 CPU 执行,以下图所示
并发和并行的关系
并发
意味着应用程序会执行多个的任务,可是若是计算机只有一个 CPU 的话,那么应用程序没法同时执行多个的任务,可是应用程序又须要执行多个任务,因此计算机在开始执行下一个任务以前,它并无完成当前的任务,只是把状态暂存,进行任务切换,CPU 在多个任务之间进行切换,直到任务完成。以下图所示
并行
是指应用程序将其任务分解为较小的子任务,这些子任务能够并行处理,例如在多个CPU上同时进行。
优点和劣势
合理使用线程是一门艺术,合理编写一道准确无误的多线程程序更是一门艺术,若是线程使用得当,可以有效的下降程序的开发和维护成本。
Java 很好的在用户空间实现了开发工具包,并在内核空间提供系统调用来支持多线程编程,Java 支持了丰富的类库 java.util.concurrent
和跨平台的内存模型
,同时也提升了开发人员的门槛,并发一直以来是一个高阶的主题,可是如今,并发也成为了主流开发人员的必备素质。
虽然线程带来的好处不少,可是编写正确的多线程(并发)程序是一件极困难的事情,并发程序的 Bug 每每会诡异地出现又诡异的消失,在当你认为没有问题的时候它就出现了,难以定位
是并发程序的一个特征,因此在此基础上你须要有扎实的并发基本功。那么,并发为何会出现呢?
并发为何会出现
计算机世界的快速发展离不开 CPU、内存和 I/O 设备的高速发展,可是这三者一直存在速度差别性问题,咱们能够从存储器的层次结构能够看出
CPU 内部是寄存器的构造,寄存器的访问速度要高于高速缓存
,高速缓存的访问速度要高于内存,最慢的是磁盘访问。
程序是在内存中执行的,程序里大部分语句都要访问内存,有些还须要访问 I/O 设备,根据漏桶理论来讲,程序总体的性能取决于最慢的操做也就是磁盘访问速度。
由于 CPU 速度太快了,因此为了发挥 CPU 的速度优点,平衡这三者的速度差别,计算机体系机构、操做系统、编译程序都作出了贡献,主要体现为:
-
CPU 使用缓存来中和和内存的访问速度差别 -
操做系统提供进程和线程调度,让 CPU 在执行指令的同时分时复用线程,让内存和磁盘不断交互,不一样的 CPU 时间片
可以执行不一样的任务,从而均衡这三者的差别 -
编译程序提供优化指令的执行顺序,让缓存可以合理的使用
咱们在享受这些便利的同时,多线程也为咱们带来了挑战,下面咱们就来探讨一下并发问题为何会出现以及多线程的源头是什么
线程带来的安全性问题
线程安全性是很是复杂的,在没有采用同步机制
的状况下,多个线程中的执行操做每每是不可预测的,这也是多线程带来的挑战之一,下面咱们给出一段代码,来看看安全性问题体如今哪
public class TSynchronized implements Runnable{
static int i = 0;
public void increase(){
i++;
}
@Override
public void run() {
for(int i = 0;i < 1000;i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
TSynchronized tSynchronized = new TSynchronized();
Thread aThread = new Thread(tSynchronized);
Thread bThread = new Thread(tSynchronized);
aThread.start();
bThread.start();
System.out.println("i = " + i);
}
}
这段程序输出后会发现,i 的值每次都不同,这不符合咱们的预测,那么为何会出现这种状况呢?咱们先来分析一下程序的运行过程。
TSynchronized
实现了 Runnable 接口,并定义了一个静态变量 i
,而后在 increase
方法中每次都增长 i 的值,在其实现的 run 方法中进行循环调用,共执行 1000 次。
可见性问题
在单核 CPU 时代,全部的线程共用一个 CPU,CPU 缓存和内存的一致性问题容易解决,CPU 和 内存之间
若是用图来表示的话我想会是下面这样
在多核时代,由于有多核的存在,每一个核都可以独立的运行一个线程,每颗 CPU 都有本身的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不一样的 CPU 上执行时,这些线程操做的是不一样的 CPU 缓存
由于 i 是静态变量,没有通过任何线程安全措施的保护,多个线程会并发修改 i 的值,因此咱们认为 i 不是线程安全的,致使这种结果的出现是因为 aThread 和 bThread 中读取的 i 值彼此不可见,因此这是因为 可见性
致使的线程安全问题。
原子性问题
看起来很普通的一段程序却由于两个线程 aThread
和 bThread
交替执行产生了不一样的结果。可是根源不是由于建立了两个线程致使的,多线程只是产生线程安全性的必要条件,最终的根源出如今 i++
这个操做上。
这个操做怎么了?这不就是一个给 i 递增的操做吗?也就是 「i++ => i = i + 1」,这怎么就会产生问题了?
由于 i++
不是一个 原子性
操做,仔细想一下,i++ 其实有三个步骤,读取 i 的值,执行 i + 1 操做,而后把 i + 1 得出的值从新赋给 i(将结果写入内存)。
当两个线程开始运行后,每一个线程都会把 i 的值读入到 CPU 缓存中,而后执行 + 1 操做,再把 + 1 以后的值写入内存。由于线程间都有各自的虚拟机栈和程序计数器,他们彼此之间没有数据交换,因此当 aThread 执行 + 1 操做后,会把数据写入到内存,同时 bThread 执行 + 1 操做后,也会把数据写入到内存,由于 CPU 时间片的执行周期是不肯定的,因此会出现当 aThread 尚未把数据写入内存时,bThread 就会读取内存中的数据,而后执行 + 1操做,再写回内存,从而覆盖 i 的值,致使 aThread 所作的努力白费。
为何上面的线程切换会出现问题呢?
咱们先来考虑一下正常状况下(即不会出现线程安全性问题的状况下)两条线程的执行顺序
能够看到,当 aThread 在执行完整个 i++ 的操做后,操做系统对线程进行切换,由 aThread -> bThread,这是最理想的操做,一旦操做系统在任意 读取/增长/写入
阶段产生线程切换,都会产生线程安全问题。例如以下图所示
最开始的时候,内存中 i = 0,aThread 读取内存中的值并把它读取到本身的寄存器中,执行 +1 操做,此时发生线程切换,bThread 开始执行,读取内存中的值并把它读取到本身的寄存器中,此时发生线程切换,线程切换至 aThread 开始运行,aThread 把本身寄存器的值写回到内存中,此时又发生线程切换,由 aThread -> bThread,线程 bThread 把本身寄存器的值 +1 而后写回内存,写完后内存中的值不是 2 ,而是 1, 内存中的 i 值被覆盖了。
咱们上面提到 原子性
这个概念,那么什么是原子性呢?
❝并发编程的原子性操做是彻底独立于任何其余进程运行的操做,原子操做多用于现代操做系统和并行处理系统中。
原子操做一般在内核中使用,由于内核是操做系统的主要组件。可是,大多数计算机硬件,编译器和库也提供原子性操做。
在加载和存储中,计算机硬件对存储器字进行读取和写入。为了对值进行匹配、增长或者减少操做,通常经过原子操做进行。在原子操做期间,处理器能够在同一数据传输期间完成读取和写入。这样,其余输入/输出机制或处理器没法执行存储器读取或写入任务,直到原子操做完成为止。
❞
简单来说,就是「原子操做要么所有执行,要么所有不执行」。数据库事务的原子性也是基于这个概念演进的。
有序性问题
在并发编程中还有带来让人很是头疼的 有序性
问题,有序性顾名思义就是顺序性,在计算机中指的就是指令的前后执行顺序。一个很是显而易见的例子就是 JVM 中的类加载
这是一个 JVM 加载类的过程图,也称为类的生命周期,类从加载到 JVM 到卸载一共会经历五个阶段 「加载、链接、初始化、使用、卸载」。这五个过程的执行顺序是必定的,可是在链接阶段,也会分为三个过程,即 「验证、准备、解析」 阶段,这三个阶段的执行顺序不是肯定的,一般交叉进行,在一个阶段的执行过程当中会激活另外一个阶段。
有序性问题通常是编译器带来的,编译器有的时候确实是 「好心办坏事」,它为了优化系统性能,每每更换指令的执行顺序。
活跃性问题
多线程还会带来活跃性
问题,如何定义活跃性问题呢?活跃性问题关注的是 「某件事情是否会发生」。
「若是一组线程中的每一个线程都在等待一个事件的发生,而这个事件只能由该组中正在等待的线程触发,这种状况会致使死锁」。
简单一点来表述一下,就是每一个线程都在等待其余线程释放资源,而其余资源也在等待每一个线程释放资源,这样没有线程抢先释放本身的资源,这种状况会产生死锁,全部线程都会无限的等待下去。
「死锁的必要条件」
形成死锁的缘由有四个,破坏其中一个便可破坏死锁
-
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。若是此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程释放。 -
请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对本身已得到的其它资源保持占有。 -
不剥夺条件:指进程已得到的资源,在未使用完以前,不能被剥夺,只能在使用完时由本身释放。 -
循环等待:指在发生死锁时,必然存在一个进程对应的环形链。
换句话说,死锁线程集合中的每一个线程都在等待另外一个死锁线程占有的资源。可是因为全部线程都不能运行,它们之中任何一个资源都没法释放资源,因此没有一个线程能够被唤醒。
若是说死锁很痴情
的话,那么活锁
用一则成语来表示就是 弄巧成拙
。
某些状况下,当线程意识到它不能获取所须要的下一个锁时,就会尝试礼貌的释放已经得到的锁,而后等待很是短的时间再次尝试获取。能够想像一下这个场景:当两我的在狭路相逢的时候,都想给对方让路,相同的步调会致使双方都没法前进。
如今假想有一对并行的线程用到了两个资源。它们分别尝试获取另外一个锁失败后,两个线程都会释放本身持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程当中没有线程阻塞,可是线程仍然不会向下执行,这种情况咱们称之为 活锁(livelock)
。
若是咱们指望的事情一直不会发生,就会产生活跃性问题,好比单线程中的无限循环
while(true){...}
for(;;){}
在多线程中,好比 aThread 和 bThread 都须要某种资源,aThread 一直占用资源不释放,bThread 一直得不到执行,就会形成活跃性问题,bThread 线程会产生饥饿
,咱们后面会说。
性能问题
与活跃性问题密切相关的是 性能
问题,若是说活跃性问题关注的是最终的结果,那么性能问题关注的就是形成结果的过程,性能问题有不少方面:好比「服务时间过长,吞吐率太低,资源消耗太高」,在多线程中这样的问题一样存在。
在多线程中,有一个很是重要的性能因素那就是咱们上面提到的 线程切换
,也称为 上下文切换(Context Switch)
,这种操做开销很大。
❝在计算机世界中,老外都喜欢用 context 上下文这个词,这个词涵盖的内容不少,包括上下文切换的资源,寄存器的状态、程序计数器等。context switch 通常指的就是这些上下文切换的资源、寄存器状态、程序计数器的变化等。
❞
在上下文切换中,会保存和恢复上下文,丢失局部性,把大量的时间消耗在线程切换上而不是线程运行上。
为何线程切换会开销如此之大呢?线程间的切换会涉及到如下几个步骤
将 CPU 从一个线程切换到另外一线程涉及挂起当前线程,保存其状态,例如寄存器,而后恢复到要切换的线程的状态,加载新的程序计数器,此时线程切换实际上就已经完成了;此时,CPU 不在执行线程切换代码,进而执行新的和线程关联的代码。
引发线程切换的几种方式
线程间的切换通常是操做系统层面须要考虑的问题,那么引发线程上下文切换有哪几种方式呢?或者说线程切换有哪几种诱因呢?主要有下面几种引发上下文切换的方式
-
当前正在执行的任务完成,系统的 CPU 正常调度下一个须要运行的线程 -
当前正在执行的任务遇到 I/O 等阻塞操做,线程调度器挂起此任务,继续调度下一个任务。 -
多个任务并发抢占锁资源,当前任务没有得到锁资源,被线程调度器挂起,继续调度下一个任务。 -
用户的代码挂起当前任务,好比线程执行 sleep 方法,让出CPU。 -
使用硬件中断的方式引发上下文切换

3
线程安全性
在 Java 中,要实现线程安全性,必需要正确的使用线程和锁,可是这些只是知足线程安全的一种方式,要编写正确无误的线程安全的代码,其核心就是对状态访问操做进行管理。最重要的就是最 共享(Shared)
的 和 可变(Mutable)
的状态。只有共享和可变的变量才会出现问题,私有变量不会出现问题,参考程序计数器
。
对象的状态能够理解为存储在实例变量或者静态变量中的数据,共享意味着某个变量能够被多个线程同时访问、可变意味着变量在生命周期内会发生变化。一个变量是不是线程安全的,取决于它是否被多个线程访问。要使变量可以被安全访问,必须经过同步机制来对变量进行修饰。
若是不采用同步机制的话,那么就要避免多线程对共享变量的访问,主要有下面两种方式
-
不要在多线程之间共享变量 -
将共享变量置为不可变的
咱们说了这么屡次线程安全性,那么什么是线程安全性呢?
什么是线程安全性
多个线程能够同时安全调用的代码称为线程安全的,若是一段代码是安全的,那么这段代码就不存在 竞态条件
。仅仅当多个线程共享资源时,才会出现竞态条件。
根据上面的探讨,咱们能够得出一个简单的结论:「当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的」。
单线程就是一个线程数量为 1 的多线程,单线程必定是线程安全的。读取某个变量的值不会产生安全性问题,由于无论读取多少次,这个变量的值都不会被修改。
原子性
咱们上面提到了原子性的概念,你能够把原子性
操做想象成为一个不可分割
的总体,它的结果只有两种,要么所有执行,要么所有回滚。你能够把原子性认为是 婚姻关系
的一种,男人和女人只会产生两种结果,好好的
和 说散就散
,通常男人的一辈子均可以把他当作是原子性的一种,固然咱们不排除时间管理(线程切换)
的个例,咱们知道线程切换必然会伴随着安全性问题,男人要出去浪也会形成两种结果,这两种结果分别对应安全性的两个结果:线程安全(好好的)和线程不安全(说散就散)。
竞态条件
有了上面的线程切换的功底,那么竞态条件也就好定义了,它指的就是「两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(race condition)」 ,线程切换是致使竞态条件出现的诱导因素,咱们经过一个示例来讲明,来看一段代码
public class RaceCondition {
private Signleton single = null;
public Signleton newSingleton(){
if(single == null){
single = new Signleton();
}
return single;
}
}
在上面的代码中,涉及到一个竞态条件,那就是判断 single
的时候,若是 single 判断为空,此时发生了线程切换,另一个线程执行,判断 single 的时候,也是空,执行 new 操做,而后线程切换回以前的线程,再执行 new 操做,那么内存中就会有两个 Singleton 对象。
加锁机制
在 Java 中,有不少种方式来对共享和可变的资源进行加锁和保护。Java 提供一种内置的机制对资源进行保护:synchronized
关键字,它有三种保护机制
-
对方法进行加锁,确保多个线程中只有一个线程执行方法; -
对某个对象实例(在咱们上面的探讨中,变量可使用对象来替换)进行加锁,确保多个线程中只有一个线程对对象实例进行访问; -
对类对象进行加锁,确保多个线程只有一个线程可以访问类中的资源。
synchronized 关键字对资源进行保护的代码块俗称 同步代码块(Synchronized Block)
,例如
synchronized(lock){
// 线程安全的代码
}
每一个 Java 对象均可以用作一个实现同步的锁,这些锁被称为 内置锁(Instrinsic Lock)
或者 监视器锁(Monitor Lock)
。线程在进入同步代码以前会自动得到锁,而且在退出同步代码时自动释放锁,而不管是经过正常执行路径退出仍是经过异常路径退出,得到内置锁的惟一途径就是进入这个由锁保护的同步代码块或方法。
synchronized 的另外一种隐含的语义就是 互斥
,互斥意味着独占
,最多只有一个线程持有锁,当线程 A 尝试得到一个由线程 B 持有的锁时,线程 A 必须等待或者阻塞,直到线程 B 释放这个锁,若是线程 B 不释放锁的话,那么线程 A 将会一直等待下去。
线程 A 得到线程 B 持有的锁时,线程 A 必须等待或者阻塞,可是获取锁的线程 B 能够重入,重入的意思能够用一段代码表示
public class Retreent {
public synchronized void doSomething(){
doSomethingElse();
System.out.println("doSomething......");
}
public synchronized void doSomethingElse(){
System.out.println("doSomethingElse......");
}
获取 doSomething() 方法锁的线程能够执行 doSomethingElse() 方法,执行完毕后能够从新执行 doSomething() 方法中的内容。锁重入也支持子类和父类之间的重入,具体的咱们后面会进行介绍。
volatile
是一种轻量级的 synchronized
,也就是一种轻量级的加锁方式,volatile 经过保证共享变量的可见性来从侧面对对象进行加锁。可见性的意思就是当一个线程修改一个共享变量时,另一个线程可以 看见
这个修改的值。volatile 的执行成本要比 synchronized
低不少,由于 volatile 不会引发线程的上下文切换。
咱们还可使用原子类
来保证线程安全,原子类其实就是 rt.jar
下面以 atomic
开头的类
除此以外,咱们还可使用 java.util.concurrent
工具包下的线程安全的集合类来确保线程安全,具体的实现类和其原理咱们后面会说。
可使用不一样的并发模型来实现并发系统,并发模型说的是系统中的线程如何协做完成并发任务。不一样的并发模型以不一样的方式拆分任务,线程能够以不一样的方式进行通讯和协做。

4
竞态条件和关键区域
竞态条件是在关键代码区域发生的一种特殊条件。关键区域是由多个线程同时执行的代码部分,关键区域中的代码执行顺序会对形成不同的结果。若是多个线程执行一段关键代码,而这段关键代码会由于执行顺序不一样而形成不一样的结果时,那么这段代码就会包含竞争条件。

5
并发模型和分布式系统很类似
并发模型其实和分布式系统模型很是类似,在并发模型中是线程
彼此进行通讯,而在分布式系统模型中是 进程
彼此进行通讯。然而本质上,进程和线程也很是类似。这也就是为何并发模型和分布式模型很是类似的缘由。
分布式系统一般要比并发系统面临更多的挑战和问题好比进程通讯、网络可能出现异常,或者远程机器挂掉等等。可是一个并发模型一样面临着好比 CPU 故障、网卡出现问题、硬盘出现问题等。
由于并发模型和分布式模型很类似,所以他们能够相互借鉴,例如用于线程分配的模型就相似于分布式系统环境中的负载均衡模型。
其实说白了,分布式模型的思想就是借鉴并发模型的基础上推演发展来的。

6
认识两个状态
并发模型的一个重要的方面是,线程是否应该共享状态
,是具备共享状态
仍是独立状态
。共享状态也就意味着在不一样线程之间共享某些状态
状态其实就是数据
,好比一个或者多个对象。当线程要共享数据时,就会形成 竞态条件
或者 死锁
等问题。固然,这些问题只是可能会出现,具体实现方式取决于你是否安全的使用和访问共享对象。
独立的状态代表状态不会在多个线程之间共享,若是线程之间须要通讯的话,他们能够访问不可变的对象来实现,这是最有效的避免并发问题的一种方式,以下图所示
使用独立状态让咱们的设计更加简单,由于只有一个线程可以访问对象,即便交换对象,也是不可变的对象。

7
并发模型
并行 Worker
第一个并发模型是并行 worker 模型,客户端会把任务交给 代理人(Delegator)
,而后由代理人把工做分配给不一样的 工人(worker)
。以下图所示
并行 worker 的核心思想是,它主要有两个进程即代理人和工人,Delegator 负责接收来自客户端的任务并把任务下发,交给具体的 Worker 进行处理,Worker 处理完成后把结果返回给 Delegator,在 Delegator 接收到 Worker 处理的结果后对其进行汇总,而后交给客户端。
并行 Worker 模型是 Java 并发模型中很是常见的一种模型。许多 java.util.concurrent
包下的并发工具都使用了这种模型。
并行 Worker 的优势
并行 Worker 模型的一个很是明显的特色就是很容易理解,为了提升系统的并行度你能够增长多个 Worker 完成任务。
并行 Worker 模型的另一个好处就是,它会将一个任务拆分红多个小任务,并发执行,Delegator 在接受到 Worker 的处理结果后就会返回给 Client,整个 Worker -> Delegator -> Client 的过程是异步
的。
并行 Worker 的缺点
一样的,并行 Worker 模式一样会有一些隐藏的缺点
「共享状态会变得很复杂」
实际的并行 Worker 要比咱们图中画出的更复杂,主要是并行 Worker 一般会访问内存或共享数据库中的某些共享数据。
这些共享状态可能会使用一些工做队列来保存业务数据、数据缓存、数据库的链接池等。在线程通讯中,线程须要确保共享状态是否可以让其余线程共享,而不是仅仅停留在 CPU 缓存中让本身可用,固然这些都是程序员在设计时就须要考虑的问题。线程须要避免 竞态条件
,死锁
和许多其余共享状态形成的并发问题。
多线程在访问共享数据时,会丢失并发性,由于操做系统要保证只有一个线程可以访问数据,这会致使共享数据的争用和抢占。未抢占到资源的线程会 阻塞
。
现代的非阻塞并发算法能够减小争用提升性能,可是非阻塞算法比较难以实现。
可持久化的数据结构(Persistent data structures)
是另一个选择。可持久化的数据结构在修改后始终会保留先前版本。所以,若是多个线程同时修改一个可持久化的数据结构,而且一个线程对其进行了修改,则修改的线程会得到对新数据结构的引用。
虽然可持久化的数据结构是一个新的解决方法,可是这种方法实行起来却有一些问题,好比,一个持久列表会将新元素添加到列表的开头,并返回所添加的新元素的引用,可是其余线程仍然只持有列表中先前的第一个元素的引用,他们看不到新添加的元素。
持久化的数据结构好比 链表(LinkedList)
在硬件性能上表现不佳。列表中的每一个元素都是一个对象,这些对象散布在计算机内存中。现代 CPU 的顺序访问每每要快的多,所以使用数组等顺序访问的数据结构则可以得到更高的性能。CPU 高速缓存能够将一个大的矩阵块加载到高速缓存中,并让 CPU 在加载后直接访问 CPU 高速缓存中的数据。对于链表,将元素分散在整个 RAM 上,这其实是不可能的。
「无状态的 worker」
共享状态能够由其余线程所修改,所以,worker 必须在每次操做共享状态时从新读取,以确保在副本上可以正确工做。不在线程内部保持状态的 worker 成为无状态的 worker。
「做业顺序是不肯定的」
并行工做模型的另外一个缺点是做业的顺序不肯定,没法保证首先执行或最后执行哪些做业。任务 A 在任务 B 以前分配给 worker,可是任务 B 可能在任务 A 以前执行。
流水线
第二种并发模型就是咱们常常在生产车间遇到的 流水线并发模型
,下面是流水线设计模型的流程图
这种组织架构就像是工厂中装配线中的 worker,每一个 worker 只完成所有工做的一部分,完成一部分后,worker 会将工做转发给下一个 worker。
每道程序都在本身的线程中运行,彼此之间不会共享状态,这种模型也被称为无共享并发模型。
使用流水线并发模型一般被设计为非阻塞I/O
,也就是说,当没有给 worker 分配任务时,worker 会作其余工做。非阻塞I/O 意味着当 worker 开始 I/O 操做,例如从网络中读取文件,worker 不会等待 I/O 调用完成。由于 I/O 操做很慢,因此等待 I/O 很是耗费时间。在等待 I/O 的同时,CPU 能够作其余事情,I/O 操做完成后的结果将传递给下一个 worker。下面是非阻塞 I/O 的流程图
在实际状况中,任务一般不会按着一条装配线流动,因为大多数程序须要作不少事情,所以须要根据完成的不一样工做在不一样的 worker 之间流动,以下图所示
任务还可能须要多个 worker 共同参与完成
响应式 - 事件驱动系统
使用流水线模型的系统有时也被称为 响应式
或者 事件驱动系统
,这种模型会根据外部的事件做出响应,事件多是某个 HTTP 请求或者某个文件完成加载到内存中。
Actor 模型
在 Actor 模型中,每个 Actor 其实就是一个 Worker, 每个 Actor 都可以处理任务。
简单来讲,Actor 模型是一个并发模型,它定义了一系列系统组件应该如何动做和交互的通用规则,最著名的使用这套规则的编程语言是 Erlang。一个参与者Actor
对接收到的消息作出响应,而后能够建立出更多的 Actor 或发送更多的消息,同时准备接收下一条消息。
Channels 模型
在 Channel 模型中,worker 一般不会直接通讯,与此相对的,他们一般将事件发送到不一样的 通道(Channel)
上,而后其余 worker 能够在这些通道上获取消息,下面是 Channel 的模型图
有的时候 worker 不须要明确知道接下来的 worker 是谁,他们只须要将做者写入通道中,监听 Channel 的 worker 能够订阅或者取消订阅,这种方式下降了 worker 和 worker 之间的耦合性。
流水线设计的优势
与并行设计模型相比,流水线模型具备一些优点,具体优点以下
「不会存在共享状态」
由于流水线设计可以保证 worker 在处理完成后再传递给下一个 worker,因此 worker 与 worker 之间不须要共享任何状态,也就无需考虑并发问题。你甚至能够在实现上把每一个 worker 当作是单线程的一种。
「有状态 worker」
由于 worker 知道没有其余线程修改自身的数据,因此流水线设计中的 worker 是有状态的,有状态的意思是他们能够将须要操做的数据保留在内存中,有状态一般比无状态更快。
「更好的硬件整合」
由于你能够把流水线当作是单线程的,而单线程的工做优点在于它可以和硬件的工做方式相同。由于有状态的 worker 一般在 CPU 中缓存数据,这样能够更快地访问缓存的数据。
「使任务更加有效的进行」
能够对流水线并发模型中的任务进行排序,通常用来日志的写入和恢复。
流水线设计的缺点
流水线并发模型的缺点是任务会涉及多个 worker,所以可能会分散在项目代码的多个类中。所以很难肯定每一个 worker 都在执行哪一个任务。流水线的代码编写也比较困难,设计许多嵌套回调处理程序的代码一般被称为 回调地狱
。回调地狱很难追踪 debug。

8
函数性并行
函数性并行模型是最近才提出的一种并发模型,它的基本思路是使用函数调用来实现。消息的传递就至关因而函数的调用。传递给函数的参数都会被拷贝,所以在函数以外的任何实体都没法操纵函数内的数据。这使得函数执行相似于原子
操做。每一个函数调用均可以独立于任何其余函数调用执行。
当每一个函数调用独立执行时,每一个函数均可以在单独的 CPU 上执行。这也就是说,函数式并行并行至关因而各个 CPU 单独执行各自的任务。
JDK 1.7 中的 ForkAndJoinPool
类就实现了函数性并行的功能。Java 8 提出了 stream 的概念,使用并行流也可以实现大量集合的迭代。
函数性并行的难点是要知道函数的调用流程以及哪些 CPU 执行了哪些函数,跨 CPU 函数调用会带来额外的开销。
咱们以前说过,线程就是进程中的一条顺序流
,在 Java 中,每一条 Java 线程就像是 JVM 的一条顺序流,就像是虚拟 CPU 同样来执行代码。Java 中的 main()
方法是一条特殊的线程,JVM 建立的 main 线程是一条主执行线程
,在 Java 中,方法都是由 main 方法发起的。在 main 方法中,你照样能够建立其余的线程
(执行顺序流),这些线程能够和 main 方法共同执行应用代码。
Java 线程也是一种对象,它和其余对象同样。Java 中的 Thread 表示线程,Thread 是 java.lang.Thread
类或其子类的实例。那么下面咱们就来一块儿探讨一下在 Java 中如何建立和启动线程。

9
建立并启动线程
在 Java 中,建立线程的方式主要有三种
-
经过继承 Thread
类来建立线程 -
经过实现 Runnable
接口来建立线程 -
经过 Callable
和Future
来建立线程
下面咱们分别探讨一下这几种建立方式
继承 Thread 类来建立线程
第一种方式是继承 Thread 类来建立线程,以下示例
public class TJavaThread extends Thread{
static int count;
@Override
public synchronized void run() {
for(int i = 0;i < 10000;i++){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
TJavaThread tJavaThread = new TJavaThread();
tJavaThread.start();
tJavaThread.join();
System.out.println("count = " + count);
}
}
线程的主要建立步骤以下
-
定义一个线程类使其继承 Thread 类,并重写其中的 run 方法,run 方法内部就是线程要完成的任务,所以 run 方法也被称为 执行体
-
建立了 Thread 的子类,上面代码中的子类是 TJavaThread
-
启动方法须要注意,并非直接调用 run
方法来启动线程,而是使用start
方法来启动线程。固然 run 方法能够调用,这样的话就会变成普通方法调用,而不是新建立一个线程来调用了。
public static void main(String[] args) throws InterruptedException {
TJavaThread tJavaThread = new TJavaThread();
tJavaThread.run();
System.out.println("count = " + count);
}
这样的话,整个 main 方法只有一条执行线程也就是 main 线程,由两条执行线程变为一条执行线程
Thread 构造器只须要一个 Runnable 对象,调用 Thread 对象的 start() 方法为该线程执行必须的初始化操做,而后调用 Runnable 的 run 方法,以便在这个线程中启动任务。咱们上面使用了线程的 join
方法,它用来等待线程的执行结束,若是咱们不加 join 方法,它就不会等待 tJavaThread 的执行完毕,输出的结果可能就不是 10000
能够看到,在 run 方法尚未结束前,run 就被返回了。也就是说,程序不会等到 run 方法执行完毕就会执行下面的指令。
使用继承方式建立线程的优点:编写比较简单;可使用 this
关键字直接指向当前线程,而无需使用 Thread.currentThread()
来获取当前线程。
使用继承方式建立线程的劣势:在 Java 中,只容许单继承(拒绝肛精说使用内部类能够实现多继承)的原则,因此使用继承的方式,子类就不能再继承其余类。
使用 Runnable 接口来建立线程
相对的,还可使用 Runnable
接口来建立线程,以下示例
public class TJavaThreadUseImplements implements Runnable{
static int count;
@Override
public synchronized void run() {
for(int i = 0;i < 10000;i++){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new TJavaThreadUseImplements()).start();
System.out.println("count = " + count);
}
}
线程的主要建立步骤以下
-
首先定义 Runnable 接口,并重写 Runnable 接口的 run 方法,run 方法的方法体一样是该线程的线程执行体。 -
建立线程实例,可使用上面代码这种简单的方式建立,也能够经过 new 出线程的实例来建立,以下所示
TJavaThreadUseImplements tJavaThreadUseImplements = new TJavaThreadUseImplements();
new Thread(tJavaThreadUseImplements).start();
-
再调用线程对象的 start 方法来启动该线程。
线程在使用实现 Runnable
的同时也能实现其余接口,很是适合多个相同线程来处理同一份资源的状况,体现了面向对象的思想。
使用 Runnable 实现的劣势是编程稍微繁琐,若是要访问当前线程,则必须使用 Thread.currentThread()
方法。
使用 Callable 接口来建立线程
Runnable 接口执行的是独立的任务,Runnable 接口不会产生任何返回值,若是你但愿在任务完成后可以返回一个值的话,那么你能够实现 Callable
接口而不是 Runnable 接口。Java SE5 引入了 Callable 接口,它的示例以下
public class CallableTask implements Callable {
static int count;
public CallableTask(int count){
this.count = count;
}
@Override
public Object call() {
return count;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> {
for(int i = 0;i < 1000;i++){
count++;
}
return count;
});
Thread thread = new Thread(task);
thread.start();
Integer total = task.get();
System.out.println("total = " + total);
}
}
我想,使用 Callable 接口的好处你已经知道了吧,既可以实现多个接口,也可以获得执行结果的返回值。Callable 和 Runnable 接口仍是有一些区别的,主要区别以下
-
Callable 执行的任务有返回值,而 Runnable 执行的任务没有返回值 -
Callable(重写)的方法是 call 方法,而 Runnable(重写)的方法是 run 方法。 -
call 方法能够抛出异常,而 Runnable 方法不能抛出异常
使用线程池来建立线程
首先先来认识一下顶级接口 Executor
,Executor 虽然不是传统线程建立的方式之一,可是它却成为了建立线程的替代者,使用线程池的好处以下
-
利用线程池可以复用线程、控制最大并发数。 -
实现任务线程队列 缓存策略
和拒绝机制
。 -
实现某些与时间相关的功能,如定时执行、周期执行等。 -
隔离线程环境。好比,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;所以,经过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免服务线程互相影响。
你可使用以下操做来替换线程建立
new Thread(new(RunnableTask())).start()
// 替换为
Executor executor = new ExecutorSubClass() // 线程池实现类;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
ExecutorService
是 Executor 的默认实现,也是 Executor 的扩展接口,ThreadPoolExecutor 类提供了线程池的扩展实现。Executors
类为这些 Executor 提供了方便的工厂方法。下面是使用 ExecutorService 建立线程的几种方式
CachedThreadPool
从而简化了并发编程。Executor 在客户端和任务之间提供了一个间接层;与客户端直接执行任务不一样,这个中介对象将执行任务。Executor 容许你管理异步
任务的执行,而无须显示地管理线程的生命周期。
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for(int i = 0;i < 5;i++){
service.execute(new TestThread());
}
service.shutdown();
}
CachedThreadPool
会为每一个任务都建立一个线程。
❝注意:ExecutorService 对象是使用静态的
❞Executors
建立的,这个方法能够肯定 Executor 类型。对shutDown
的调用能够防止新任务提交给 ExecutorService ,这个线程在 Executor 中全部任务完成后退出。
FixedThreadPool
FixedThreadPool 使你可使用有限
的线程集来启动多线程
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i = 0;i < 5;i++){
service.execute(new TestThread());
}
service.shutdown();
}
有了 FixedThreadPool 使你能够一次性的预先执行高昂的线程分配,所以也就能够限制线程的数量。这能够节省时间,由于你没必要为每一个任务都固定的付出建立线程的开销。
SingleThreadExecutor
SingleThreadExecutor 就是线程数量为 1
的 FixedThreadPool,若是向 SingleThreadPool 一次性提交了多个任务,那么这些任务将会排队,每一个任务都会在下一个任务开始前结束,全部的任务都将使用相同的线程。SingleThreadPool 会序列化全部提交给他的任务,并会维护它本身(隐藏)的悬挂队列。
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
for(int i = 0;i < 5;i++){
service.execute(new TestThread());
}
service.shutdown();
}
从输出的结果就能够看到,任务都是挨着执行的。我为任务分配了五个线程,可是这五个线程不像是咱们以前看到的有换进换出的效果,它每次都会先执行完本身的那个线程,而后余下的线程继续走完
这条线程的执行路径。你能够用 SingleThreadExecutor 来确保任意时刻都只有惟一一个任务在运行。
休眠
影响任务行为的一种简单方式就是使线程 休眠,选定给定的休眠时间,调用它的 sleep()
方法, 通常使用的TimeUnit
这个时间类替换 Thread.sleep()
方法,示例以下:
public class SuperclassThread extends TestThread{
@Override
public void run() {
System.out.println(Thread.currentThread() + "starting ..." );
try {
for(int i = 0;i < 5;i++){
if(i == 3){
System.out.println(Thread.currentThread() + "sleeping ...");
TimeUnit.MILLISECONDS.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "wakeup and end ...");
}
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
for(int i = 0;i < 5;i++){
executors.execute(new SuperclassThread());
}
executors.shutdown();
}
}
❝关于 TimeUnit 中的 sleep() 方法和 Thread.sleep() 方法的比较,请参考下面这篇博客
(https://www.cnblogs.com/xiadongqing/p/9925567.html)
❞
优先级
上面提到线程调度器对每一个线程的执行都是不可预知的,随机执行的,那么有没有办法告诉线程调度器哪一个任务想要优先被执行呢?你能够经过设置线程的优先级状态,告诉线程调度器哪一个线程的执行优先级比较高,「请给这个骑手立刻派单」,线程调度器倾向于让优先级较高的线程优先执行,然而,这并不意味着优先级低的线程得不到执行,也就是说,优先级不会致使死锁的问题。优先级较低的线程只是执行频率较低。
public class SimplePriorities implements Runnable{
private int priority;
public SimplePriorities(int priority) {
this.priority = priority;
}
@Override
public void run() {
Thread.currentThread().setPriority(priority);
for(int i = 0;i < 100;i++){
System.out.println(this);
if(i % 10 == 0){
Thread.yield();
}
}
}
@Override
public String toString() {
return Thread.currentThread() + " " + priority;
}
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for(int i = 0;i < 5;i++){
service.execute(new SimplePriorities(Thread.MAX_PRIORITY));
}
service.execute(new SimplePriorities(Thread.MIN_PRIORITY));
}
}
toString() 方法被覆盖,以便经过使用 Thread.toString()
方法来打印线程的名称。你能够改写线程的默认输出,这里采用了 「Thread[pool-1-thread-1,10,main]」 这种形式的输出。
经过输出,你能够看到,最后一个线程的优先级最低,其他的线程优先级最高。注意,优先级是在 run 开头设置的,在构造器中设置它们不会有任何好处,由于这个时候线程尚未执行任务。
尽管 JDK 有 10 个优先级,可是通常只有「MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY」 三种级别。
做出让步
咱们上面提过,若是知道一个线程已经在 run() 方法中运行的差很少了,那么它就能够给线程调度器一个提示:我已经完成了任务中最重要的部分,可让给别的线程使用 CPU 了。这个暗示将经过 yield() 方法做出。
❝有一个很重要的点就是,Thread.yield() 是建议执行切换CPU,而不是强制执行CPU切换。
❞
对于任何重要的控制或者在调用应用时,都不能依赖于 yield()
方法,实际上, yield() 方法常常被滥用。
后台线程
后台(daemon)
线程,是指运行时在后台提供的一种服务线程,这种线程不是属于必须的。当全部非后台线程结束时,程序也就中止了,**同时会终止全部的后台线程。**反过来讲,只要有任何非后台线程还在运行,程序就不会终止。
public class SimpleDaemons implements Runnable{
@Override
public void run() {
while (true){
try {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
} catch (InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
}
public static void main(String[] args) throws InterruptedException {
for(int i = 0;i < 10;i++){
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);
daemon.start();
}
System.out.println("All Daemons started");
TimeUnit.MILLISECONDS.sleep(175);
}
}
在每次的循环中会建立 10 个线程,并把每一个线程设置为后台线程,而后开始运行,for 循环会进行十次,而后输出信息,随后主线程睡眠一段时间后中止运行。在每次 run 循环中,都会打印当前线程的信息,主线程运行完毕,程序就执行完毕了。由于 daemon
是后台线程,没法影响主线程的执行。
可是当你把 daemon.setDaemon(true)
去掉时,while(true) 会进行无限循环,那么主线程一直在执行最重要的任务,因此会一直循环下去没法中止。
ThreadFactory
按须要建立线程的对象。使用线程工厂替换了 Thread 或者 Runnable 接口的硬链接,使程序可以使用特殊的线程子类,优先级等。通常的建立方式为
class SimpleThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
return new Thread(r);
}
}
❝Executors.defaultThreadFactory 方法提供了一个更有用的简单实现,它在返回以前将建立的线程上下文设置为已知值
❞
ThreadFactory
是一个接口,它只有一个方法就是建立线程的方法
public interface ThreadFactory {
// 构建一个新的线程。实现类可能初始化优先级,名称,后台线程状态和 线程组等
Thread newThread(Runnable r);
}
下面来看一个 ThreadFactory 的例子
public class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
public class DaemonFromFactory implements Runnable{
@Override
public void run() {
while (true){
try {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool(new DaemonThreadFactory());
for(int i = 0;i < 10;i++){
service.execute(new DaemonFromFactory());
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(500);
}
}
Executors.newCachedThreadPool
能够接受一个线程池对象,建立一个根据须要建立新线程的线程池,但会在它们可用时重用先前构造的线程,并在须要时使用提供的 ThreadFactory 建立新线程。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
加入一个线程
一个线程能够在其余线程上调用 join()
方法,其效果是等待一段时间直到第二个线程结束才正常执行。若是某个线程在另外一个线程 t 上调用 t.join() 方法,此线程将被挂起,直到目标线程 t 结束才回复(能够用 t.isAlive() 返回为真假判断)。
也能够在调用 join 时带上一个超时参数,来设置到期时间,时间到期,join方法自动返回。
对 join 的调用也能够被中断,作法是在线程上调用 interrupted
方法,这时须要用到 try...catch 子句
public class TestJoinMethod extends Thread{
@Override
public void run() {
for(int i = 0;i < 5;i++){
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted sleep");
}
System.out.println(Thread.currentThread() + " " + i);
}
}
public static void main(String[] args) throws InterruptedException {
TestJoinMethod join1 = new TestJoinMethod();
TestJoinMethod join2 = new TestJoinMethod();
TestJoinMethod join3 = new TestJoinMethod();
join1.start();
// join1.join();
join2.start();
join3.start();
}
}
join() 方法等待线程死亡。换句话说,它会致使当前运行的线程中止执行,直到它加入的线程完成其任务。
线程异常捕获
因为线程的本质,使你不能捕获从线程中逃逸的异常,一旦异常逃出任务的 run 方法,它就会向外传播到控制台,除非你采起特殊的步骤捕获这种错误的异常,在 Java5 以前,你能够经过线程组来捕获,可是在 Java 5 以后,就须要用 Executor 来解决问题,由于线程组不是一次好的尝试。
下面的任务会在 run 方法的执行期间抛出一个异常,而且这个异常会抛到 run 方法的外面,并且 main 方法没法对它进行捕获
public class ExceptionThread implements Runnable{
@Override
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new ExceptionThread());
}catch (Exception e){
System.out.println("eeeee");
}
}
}
为了解决这个问题,咱们须要修改 Executor 产生线程的方式,Java5 提供了一个新的接口 Thread.UncaughtExceptionHandler
,它容许你在每一个 Thread 上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()
会在线程因未捕获临近死亡时被调用。
public class ExceptionThread2 implements Runnable{
@Override
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("eh = " + t.getUncaughtExceptionHandler());
// 手动抛出异常
throw new RuntimeException();
}
}
// 实现Thread.UncaughtExceptionHandler 接口,建立异常处理器
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
public class HandlerThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("ex = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool(new HandlerThreadFactory());
service.execute(new ExceptionThread2());
}
}
在程序中添加了额外的追踪机制,用来验证工厂建立的线程会传递给UncaughtExceptionHandler
,你能够看到,未捕获的异常是经过 uncaughtException
来捕获的。
往期精选
本文分享自微信公众号 - 程序员cxuan(cxuangoodjob)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。