做网站主流网站,汕头seo网站优化,个人网站备案经验,网站建设 需求分析报告并发
1. 线程
1. 线程vs进程
进程是程序的一次执行过程#xff0c;是系统运行程序的基本单位#xff0c;因此进程是动态的。 系统运行一个程序即是一个进程从创建#xff0c;运行到消亡的过程。在 Java 中#xff0c;当我们启动 main 函数时其实就是启动了一个 JVM 的进…并发
1. 线程
1. 线程vs进程
进程是程序的一次执行过程是系统运行程序的基本单位因此进程是动态的。 系统运行一个程序即是一个进程从创建运行到消亡的过程。在 Java 中当我们启动 main 函数时其实就是启动了一个 JVM 的进程而 main 函数所在的线程就是这个进程中的一个线程也称主线程。
线程是一个比进程更小的执行单位。 一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源但每个线程有自己的程序计数器、虚拟机栈和本地方法栈所以系统在产生一个线程或是在各个线程之间做切换工作时负担要比进程小得多也正因为如此线程也被称为轻量级进程。
比较项目进程线程定义程序的一次执行过程是系统运行程序的基本单位动态的。比进程更小的执行单位多个线程共享进程的资源。系统中的作用系统运行一个程序即是一个进程从创建、运行到消亡的过程。一个进程在执行过程中可以产生多个线程。资源共享各进程独立不共享内存资源。线程共享进程的堆和方法区资源但有自己的程序计数器、虚拟机栈和本地方法栈。创建和切换负担系统创建和切换进程的负担较大。系统创建和切换线程的负担较小因此线程被称为轻量级进程。Java 中的体现启动 main 函数时启动 JVM 进程main 函数所在的线程为主线程。线程在进程内产生主线程和其他线程共享进程资源。
Java 程序天生就是多线程程序一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
总结线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的而各线程则不一定因为同一进程中的线程极有可能会相互影响。线程执行开销小但不利于资源的管理和保护而进程正相反。 一个进程中可以有多个线程。 多个线程共享进程的堆和方法区 (元空间)。 但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。 堆和方法区共享 堆是进程中最大的一块内存主要用于存放新创建的对象 (几乎所有对象都在这里分配内存) 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 程序计数器私有
为了线程切换后能恢复到正确的执行位置。 字节码解释器通过改变程序计数器来依次读取指令从而实现代码的流程控制如顺序执行、选择、循环、异常处理。 在多线程的情况下程序计数器用于记录当前线程执行的位置从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 虚拟机栈和本地方法栈私有
为了保证线程中的局部变量不被别的线程访问到。 虚拟机栈 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 本地方法栈 和虚拟机栈所发挥的作用非常相似区别是虚拟机栈为虚拟机执行 Java 方法 也就是字节码服务而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 2. Java线程 vs OS线程 用户线程由用户空间程序管理和调度的线程运行在用户空间专门给应用程序使用。 内核线程由操作系统内核管理和调度的线程运行在内核空间只有内核程序可以访问。
用户线程创建和切换成本低但不可以利用多核。内核态线程创建和切换成本高可以利用多核。 现在的 Java 线程的本质其实就是操作系统的线程。 3. 创建线程
使用多线程的方法继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。
真正的new Thread().start() 4. 线程的生命周期和状态 NEW: 初始状态线程被创建出来但没有被调用 start() 。 RUNNABLE: 运行状态线程被调用了 start()等待运行的状态。 BLOCKED阻塞状态需要等待锁释放。 WAITING等待状态表示该线程需要等待其他线程做出一些特定动作通知或中断。 TIMED_WAITING超时等待状态可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。 TERMINATED终止状态表示该线程已经运行完毕。 当线程执行 wait()方法之后线程进入 WAITING等待 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制比如通过 sleeplong millis方法或 waitlong millis方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后线程将会返回到 RUNNABLE 状态。 当线程进入 synchronized 方法/块或者调用 wait 后被 notify重新进入 synchronized 方法/块但是锁被其它线程占有这个时候线程就会进入 BLOCKED阻塞 状态。 线程在执行完了 run()方法之后将会进入到 TERMINATED终止 状态。
随着代码的执行在不同状态之间切换。 RUNNING vs READY
线程创建之后它将处于 NEW新建 状态调用 start() 方法后开始运行线程这时候处于 READY可运行 状态。可运行状态的线程获得了 CPU 时间片timeslice后就处于 RUNNING运行 状态。 5. 线程上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数而一个 CPU 核心在任意时刻只能被一个线程使用为了让这些线程都能得到有效执行CPU 采取的策略是为每个线程分配时间片并轮转的形式。
线程在执行过程中会有自己的运行条件和状态也称上下文比如上文所说到过的程序计数器栈信息等。当出现如下情况的时候线程会从占用 CPU 状态中退出。 主动让出 CPU比如调用了 sleep(), wait() 等。 时间片用完因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。 调用了阻塞类型的系统中断比如请求 IO线程被阻塞。 不会切换被终止或结束运行
这其中前三种都会发生线程切换线程切换意味着需要保存当前线程的上下文留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能因其每次需要保存信息恢复信息这将会占用 CPU内存等系统资源进行处理也就意味着效率会有一定损耗如果频繁切换就会造成整体效率低下。 6. Thread#sleep() vs Object#wait()
比较项目sleep() 方法wait() 方法锁的释放没有释放锁释放了锁用途通常用于暂停执行通常用于线程间交互/通信苏醒方式执行完成后自动苏醒需要其他线程调用同一个对象上的 notify() 或 notifyAll() 方法超时自动苏醒是是使用 wait(long timeout)所属类Thread 类的静态本地方法Object 类的本地方法
wait()让获得对象锁的线程实现等待会自动释放当前线程占有的对象锁每个对象Object都拥有对象锁。
sleep() 是让当前线程暂停执行不涉及到对象类 7. 可以直接调用Thread类的run方法吗
new 一个 Thread线程进入了新建状态。调用 start()方法会启动一个线程并使线程进入了就绪状态当分配到时间片后就可以开始运行了。
start() 会执行线程的相应准备工作然后自动执行 run() 方法的内容这是真正的多线程工作。
直接执行 run() 方法会把 run() 方法当成一个 main 线程下的普通方法去执行并不会在某个线程中执行它所以这并不是多线程工作。 2. 多线程
1. 并发vs并行 并发两个及两个以上的作业在 时间段交替单核CPU。 并行两个及两个以上的作业在 时刻多核CPU 。
最关键的点是是否是 同时 执行。 2. 同步vs异步 同步发出一个调用之后在没有得到结果之前 该调用就不可以返回一直等待。 异步调用在发出之后不用等待返回结果该调用直接返回。
是否需要等待方法执行的结果。 3. Why
算机底层 线程可以比作是轻量级的进程是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外多核 CPU 时代意味着多个线程可以同时运行这减少了线程上下文切换的开销。
当代互联网发展趋势 现在的系统动不动就要求百万级甚至千万级的并发量而多线程并发编程正是开发高并发系统的基础利用好多线程机制可以大大提高系统整体的并发能力以及性能。
单核时代多线程通过让一个线程在IO阻塞时其他线程继续使用CPU从而提高了单进程对CPU和IO系统的整体利用效率。
多核时代多线程通过让多个线程并行执行在多个CPU核心上从而显著提高了任务的执行效率。(单核时执行时间/CPU 核心数) 4. Problem?
并发编程是为了能提高程序的执行效率进而提高程序的运行速度。内存泄漏、死锁、线程不安全等等。
内存泄漏是指程序未能释放不再使用的内存导致内存资源逐渐减少的问题。
死锁是指两个或多个线程互相等待对方释放资源从而导致所有线程都无法继续执行的情况。 5. 什么是线程安全和不安全
在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。 6. 单核CPU上运行多个线程效率一定会更高吗
取决于线程类型和任务性质。
CPU 密集型和 IO 密集型。 CPU 密集型的线程主要进行计算和逻辑处理需要占用大量的 CPU 资源。 IO 密集型的线程主要进行输入输出操作如读写文件、网络通信等需要等待 IO 设备的响应而不占用太多的 CPU 资源。
任务是 CPU 密集型的那么开很多线程会影响效率增加了系统的开销如果任务是 IO 密集型的那么开很多线程会提高效率利用 CPU 在等待 IO 时的空闲时间。 3. 死锁
1. What
多个线程同时被阻塞它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞因此程序不可能正常终止。
线程 A 持有资源 2线程 B 持有资源 1他们同时都想申请对方的资源所以这两个线程就会互相等待而进入死锁状态。 互斥条件该资源任意一个时刻只由一个线程占用。 请求与保持条件一个线程因请求资源而阻塞时对已获得的资源保持不放。 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺只有自己使用完毕后才释放资源。 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 2. 预防避免
预防破坏死锁的产生的必要条件 破坏请求与保持条件一次性申请所有的资源。 破坏不剥夺条件占用部分资源的线程进一步申请其他资源时如果申请不到可以主动释放它占有的资源。 破坏循环等待条件靠按序申请资源来预防。按某一顺序申请资源释放资源则反序释放。破坏循环等待条件。 互斥不成立则死锁必然不发生。spooling假脱机技术外围设备联机并行操作使独占的设备变成可共享的设备 避免在资源分配时借助于算法比如银行家算法对资源分配进行计算评估使其进入安全状态。
系统能够按照某种线程推进顺序P1、P2、P3……Pn来为每个线程分配所需资源直到满足每个线程对资源的最大需求使每个线程都可顺利完成。称 P1、P2、P3.....Pn 序列为安全序列。 4. JMMJava 内存模型
对于一个共享变量当另一个线程对这个共享变量执行写操作后这个线程对这个共享变量的可见性。
1. CPU缓存模型
CPU 缓存则是为了解决 CPU 和内存处理速度不对等的问题。 为了解决内存缓存不一致性问题可以通过制定缓存一致协议。 2. 指令重排序
为了提升执行速度/性能计算机在执行程序代码的时候会对指令进行重排序在执行代码的时候并不一定是按照你写的代码的顺序依次执行。 编译器优化重排编译器包括 JVM、JIT 编译器等在不改变单线程程序语义的前提下重新安排语句的执行顺序。 指令并行重排现代处理器采用了指令级并行技术(Instruction-Level ParallelismILP)来将多条指令重叠执行。如果不存在数据依赖性处理器可以改变语句对应机器指令的执行顺序。
Java 源代码会经历 编译器优化重排 — 指令并行重排 — 内存系统重排 的过程最终才变成操作系统可执行的指令序列。
可以保证串行语义一致但是没有义务保证多线程间的语义也一致。 3. JMM
描述了 线程和主内存之间的关系为共享变量提供了可见性的保障。 线程 1 与线程 2 之间如果要进行通信 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。 线程 2 到主存中读取对应的共享变量的值。 4. 并发编程三大特性
性质描述实现方式原子性一次操作或者多次操作要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断要么都不执行。synchronized、各种 Lock 以及各种原子类。 synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块。 各种原子类是利用 CAS (compare and swap) 操作可能也会用到 volatile或者final关键字来保证原子操作。可见性当一个线程对共享变量进行了修改那么另外的线程都是立即可以看到修改后的最新值。synchronized、volatile 以及各种 Lock。将变量声明为 volatile 指示 JVM 这个变量是共享且不稳定的每次使用它都到主存中进行读取。有序性由于指令重排序问题代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。 5. volatile 关键字
1. 保证变量的可见性
修饰变量后表示这个变量是共享且不稳定的每次使用它都到主存中进行读取。 保证数据的可见性但不能保证数据的原子性。synchronized 关键字两者都能保证。 2. 禁止指令重排序
防止 JVM 的指令重排序对这个变量进行读写操作的时候会通过插入特定的 内存屏障 的方式来禁止指令重排序。
指令重排编译器和处理器为优化执行效率而调整指令顺序的技术。它在多线程环境中可能导致并发问题因为不同线程可能看到不一致的内存状态。通过使用volatile关键字或内存屏障可以防止这种重排确保程序按预期运行。 3. 不能保证原子性
利用 synchronized、Lock或者AtomicInteger都可以。 6. 乐观锁和悲观锁
1. What?
悲观锁
共享资源每次只一个线程使用其它线程阻塞用完后再把资源转让给其它线程。synchronizedReentrantLock 等独占锁。 高并发-锁竞争-线程阻塞-上下文切换-系统开销 可能 死锁。
乐观锁
认为共享资源每次被访问的时候不会出现问题线程可以不停地执行无需加锁也无需等待只是在提交修改的时候去验证对应的资源也就是数据是否被其它线程修改了具体方法可以使用版本号机制或 CAS 算法。 java.util.concurrent.atomic包下面的原子变量类。
特性使用场景优点缺点备注悲观锁写操作多多写场景竞争激烈避免频繁失败和重试影响性能固定的开销乐观锁写操作少多读场景竞争较少避免频繁加锁影响性能频繁失败和重试可能影响性能主要用于单个共享变量参考java.util.concurrent.atomic包中的原子变量类 2. 实现乐观锁
版本号机制 或 CAS 算法多 版本号机制
一般是在数据表中加上一个数据版本号 version 字段表示数据被修改的次数。当数据被修改时version 值会加一。当线程 A 要更新数据值时在读取数据的同时也会读取 version 值在提交更新时若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新否则重试更新操作直到更新成功。 CAS
Compare And Swap比较与交换用于实现乐观锁。 是一个原子操作底层依赖于一条 CPU 的原子指令。用一个预期值和要更新的变量值进行比较两值相等才会进行更新。 V要更新的变量值(Var) E预期值(Expected) N拟写入的新值(New)
当且仅当 V E CAS 通过原子方式用新值 N 更新 V 的值。如果不等说明已经有其它线程更新了 V则当前线程放弃更新。 3. CAS存在的问题
1. ABA问题
一个变量 V 初次读取的时候是 A 值并且在准备赋值的时候检查到它仍然是 A 值在这段时间它的值可能被改为其他值然后又改回 A。那 CAS 操作就会误认为从来没有被修改过。
解决在变量前面追加上版本号或者时间戳。
2. 循环时间长开销大
CAS 经常会用到自旋操作来进行重试也就是不成功就一直循环执行直到成功。如果长时间不成功CPU 大执行开销。
解决JVM 能支持处理器提供的 pause 指令 可以延迟流水线执行指令使 CPU 不会消耗过多的执行资源延迟的时间取决于具体实现的版本在一些处理器上延迟时间是零。 可以避免在退出循环的时候因内存顺序冲突而引起 CPU 流水线被清空从而提高 CPU 的执行效率。
3. 只能保证一个共享变量的原子操作
当操作涉及跨多个共享变量时 CAS 无效。
解决AtomicReference类来保证引用对象之间的原子性你可以把多个变量放在一个对象里来进行 CAS 操作。 7. synchronized 关键字
1. what
解决的是多个线程之间访问资源的同步性可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 2. 使用 修饰实例方法当前对象实例
给当前对象实例加锁进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() {//业务代码
} 修饰静态方法当前类
会作用于类的所有对象实例 进入同步代码前要获得 当前 class 的锁。
synchronized static void method() {//业务代码
} 因为访问静态 synchronized 方法占用的锁是当前类的锁而访问非静态 synchronized 方法占用的锁是当前实例对象锁。所以静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥。 修饰代码块指定类/对象 synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。 synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {//业务代码
} 3. 构造方法可以用 synchronized 修饰么
不能构造方法本身是线程安全的。
如果在构造方法内部涉及到共享资源的操作可以使用 synchronized 代码块。 4. synchronized vs. volatile
两个互补的存在。
比较维度volatile关键字synchronized关键字性能较好较差适用范围变量修饰方法以及代码块数据可见性能能数据原子性不能能主要用途解决变量在多个线程之间的可见性解决多个线程之间访问资源的同步性 8. ReentrantLock
1. what
实现了 Lock 接口是一个可重入且独占式的锁和 synchronized 关键字类似。不过ReentrantLock 更灵活、更强大增加了轮询、超时、中断、公平锁和非公平锁等高级功能。底层就是由 AQS 来实现的。 2. 公平锁vs非公平锁 公平锁 : 锁被释放之后先申请的线程先得到锁。性能较差一些因为公平锁为了保证时间上的绝对顺序上下文切换更频繁。 非公平锁锁被释放之后后申请的线程可能会先获取到锁是随机或者按照其他优先级排序的。性能更好但可能会导致某些线程永远无法获取到锁。 3. synchronized vs. ReentrantLock 两者都是可重入锁
可重入锁 也叫递归锁指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放当其再次想要获取这个对象的锁的时候还是可以获取的如果是不可重入锁的话就会造成死锁。
JDK 提供的所有现成的 Lock 实现类包括 synchronized 关键字锁都是可重入的。 synchronized - JVM, ReentrantLock - API
synchronized 是依赖于 JVM 实现的前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化但是这些优化都是在虚拟机层面实现的并没有直接暴露给我们。
ReentrantLock 是 JDK 层面实现的也就是 API 层面需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成所以我们可以通过查看它的源代码来看它是如何实现的。 ReentrantLock高级特性
等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待改为处理其他事情。
可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
可实现选择性通知锁可以绑定多个条件: synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现但是需要借助于Condition接口与newCondition()方法。 4. 可中断锁vs不可中断锁 可中断锁获取锁的过程中可以被中断不需要一直等到获取锁之后才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。 不可中断锁一旦线程申请了锁就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。 9. ReentrantReadWriteLock 1. what
ReentrantReadWriteLock其实是两把锁一把是 WriteLock (写锁)一把是 ReadLock读锁 。 读锁是共享锁写锁是独占锁。 读锁可以被同时读可以同时被多个线程持有而写锁最多只能同时被一个线程持有。
由于 ReentrantReadWriteLock 既可以保证多个线程同时读的效率同时又可以保证有写入操作时的线程安全。因此在读多写少的情况下使用 ReentrantReadWriteLock 能够明显提升系统性能。 一般锁进行并发控制的规则读读互斥、读写互斥、写写互斥。 读写锁进行并发控制的规则读读不互斥、读写互斥、写写互斥只有读读不互斥。 2. 共享锁vs独占锁 共享锁一把锁可以被多个线程同时获得。 独占锁一把锁只能被一个线程获得。
在线程持有读锁的情况下该线程不能取得写锁。 死锁 - 两个或以上的线程持有读锁想获取写锁 在线程持有写锁的情况下该线程可以继续获取读锁。 读锁共享
读锁不能升级为写锁会导致死锁。 10. Atomic 原子类
具有原子/原子操作特征的类。即使是在多个线程一起执行的时候一个操作一旦开始就不会被其他线程干扰。 基本、数组、引用、对象属性修改 类型。
更轻量级且高效适用于需要频繁更新共享变量的场景。 11. ThreadLocal
1. what
通常情况下我们创建的变量是可以被任何一个线程访问并修改的。
而ThreadLocal让每一个线程都有自己的专属本地变量。盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量那么访问这个变量的每个线程都会有这个变量的本地副本这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值 从而避免了线程安全问题。 2. 原理
每个Thread中都具备一个ThreadLocalMap而ThreadLocalMap可以存储以ThreadLocal为 key Object 对象为 value 的键值对。 3. 内存泄漏 弱引用与强引用相对是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用则被认为是不可访问或弱可访问的并因此可能在任何时刻被回收。 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用而 value 强引用。所以如果 ThreadLocal 没有被外部强引用的情况下在垃圾回收的时候key 会被清理掉而 value 不会被清理掉。
就会出现 key 为 null 的 Entry。假如我们不做任何措施的话value 永远无法被 GC 回收这个时候就可能会产生内存泄露。
在调用 set()、get()、remove() 方法的时候会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法。 12. 线程池
1. what
管理一系列线程的资源池。当有任务要处理时直接从线程池中获取线程来处理处理完之后线程并不会立即被销毁而是等待下一个任务。 2. why
为了减少每次获取资源的消耗提高对资源的利用率。
线程池提供了一种限制和管理资源包括执行一个任务的方式。 每个线程池还维护一些基本统计信息例如已完成任务的数量。
好处 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 提高响应速度。当任务到达时任务可以不需要等到线程创建就能立即执行。 提高线程的可管理性。线程是稀缺资源如果无限制的创建不仅会消耗系统资源还会降低系统的稳定性使用线程池可以进行统一的分配调优和监控。 3. 创建 ✅通过ThreadPoolExecutor构造函数来创建。 ❌通过 Executor 框架的工具类 Executors 来创建。 FixedThreadPool固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时线程池中若有空闲线程则立即执行。若没有则新的任务会被暂存在一个任务队列中待有线程空闲时便处理在任务队列中的任务。 SingleThreadExecutor 只有一个线程的线程池。若多余一个任务被提交到该线程池任务会被保存在一个任务队列中待线程空闲按先入先出的顺序执行队列中的任务。 CachedThreadPool 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定但若有空闲线程可以复用则会优先使用可复用的线程。若所有线程均在工作又有新的任务提交则会创建新的线程处理任务。所有线程在当前任务执行完毕后将返回线程池进行复用。 ScheduledThreadPool给定的延迟后运行任务或者定期执行任务的线程池。 4. 线程池的拒绝策略
当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时。 AbortPolicy: 抛出 RejectedExecutionException来拒绝新任务的处理。 CallerRunsPolicy: 调用执行自己的线程运行任务。承受此延迟并且你要求任何一个任务请求都要被执行 DiscardPolicy不处理新任务直接丢弃掉。 DiscardOldestPolicy将丢弃最早的未处理的任务请求。 5. CallerRunsPolicy 拒绝策略有什么风险如何解决
如果想要保证任何一个任务请求都要被执行的话那选择 CallerRunsPolicy 拒绝策略更合适一些。
非常耗时的任务且处理提交任务的线程是主线程可能会导致主线程阻塞影响程序的正常运行可能会内存溢出OOM。 解决 暂时无法处理的任务又被保存在阻塞队列BlockingQueue中。 调整线程池的maximumPoolSize 最大线程数参数。 任务持久化 6. 线程池常见的阻塞队列
新任务来的时候会先判断当前运行的线程数量是否达到核心线程数如果达到的话新任务就会被存放在队列中。 LinkedBlockingQueue无界队列FixedThreadPool 和 SingleThreadExector 。FixedThreadPool最多只能创建核心线程数的线程核心线程数和最大线程数相等SingleThreadExector只能创建一个线程核心线程数和最大线程数都是 1二者的任务队列永远不会被放满容量为 Integer.MAX_VALUE 的 。 SynchronousQueue同步队列CachedThreadPool 。SynchronousQueue 没有容量不存储元素目的是保证对于提交的任务如果有空闲线程则使用空闲线程来处理否则新建一个线程来处理任务。也就是说CachedThreadPool 的最大线程数是 Integer.MAX_VALUE 可以理解为线程数是可以无限扩展的可能会创建大量线程从而导致 OOM。 DelayedWorkQueue延迟阻塞队列ScheduledThreadPool 和 SingleThreadScheduledExecutor 。DelayedWorkQueue 的内部元素并不是按照放入的时间排序而是会按照延迟的时间长短对任务进行排序内部采用的是“堆”的数据结构可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2即永远不会阻塞最大扩容可达 Integer.MAX_VALUE所以最多只能创建核心线程数的线程。 7. 线程池处理任务流程 如果当前运行的线程数小于核心线程数那么就会新建一个线程来执行任务。 如果当前运行的线程数等于或大于核心线程数但是小于最大线程数那么就把该任务放入到任务队列里等待执行。 如果向任务队列投放任务失败任务队列已经满了但是当前运行的线程数是小于最大线程数的就新建一个线程来执行任务。 如果当前运行的线程数已经等同于最大线程数了新建线程将会使当前运行的线程超出最大线程数那么当前任务会被拒绝拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。 8. 线程池中的线程异常后销毁还是复用 execute()提交任务当任务通过execute()提交到线程池并在执行过程中抛出异常时如果这个异常没有在任务内被捕获那么该异常会导致当前线程终止并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止并创建一个新线程来替换它从而保持配置的线程数不变。 submit()提交任务对于通过submit()提交的任务如果在任务执行中发生异常这个异常不会直接打印出来。相反异常会被封装在由submit()返回的Future对象中。当调用Future.get()方法时可以捕获到一个ExecutionException。在这种情况下线程不会因为异常而终止它会继续存在于线程池中准备执行后续的任务。
使用execute()时未捕获异常导致线程终止线程池创建新线程替代不需要关注执行结果 使用submit()时异常被封装在Future中线程继续复用。更灵活的错误处理机制 9. 其他
1. 命名
设置线程池名称前缀有利于定位问题。
ThreadFactoryBuilder或者自己实现 ThreadFactory。 2. 线程池大小 过小如果同一时间有大量任务/请求需要处理可能会导致大量的请求/任务在任务队列中排队等待执行甚至会出现任务队列满了之后任务/请求无法处理的情况或者大量任务堆积在任务队列导致 OOM。CPU利用不充分 过大大量线程可能会同时在争取 CPU 资源这样会导致大量的上下文切换从而增加线程的执行时间影响了整体执行效率。
公式 CPU 密集型任务(N1) 这种任务消耗的主要是 CPU 资源可以将线程数设置为 NCPU 核心数1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断或者其它原因导致的任务暂停而带来的影响。一旦任务暂停CPU 就会处于空闲状态而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 I/O 密集型任务(2N) 这种任务应用起来系统会用大部分的时间来处理 I/O 交互而线程在处理 I/O 的时间段内不会占用 CPU 来处理这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中我们可以多配置一些线程具体的计算方法是 2N。 CPU 密集型利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。
IO 密集型但凡涉及到网络读取文件读取。这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少大部分时间都花在了等待 IO 操作完成上。 3. 动态修改线程池参数
三个核心参数 corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。 maximumPoolSize : 当队列中存放的任务达到队列容量的时候当前可以同时运行的线程数量变为最大线程数。 workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数如果达到的话新任务就会被存放在队列中。
借助开源工具。 4. 设计一个根据任务优先级来执行的线程池
不同的线程池会选用不同的阻塞队列作为任务队列。
使用 PriorityBlockingQueue 优先级阻塞队列作为任务队列。 风险与问题 PriorityBlockingQueue 是无界的可能堆积大量的请求从而导致 OOM。 可能会导致饥饿问题即低优先级的任务长时间得不到执行。 由于需要对队列中的元素进行排序操作以及保证线程安全并发控制采用的是可重入锁 ReentrantLock因此会降低性能。 13. Future
异步思想的典型运用主要用在一些需要执行耗时任务的场景避免程序一直原地等待耗时任务执行完成执行效率太低。
将这个耗时任务交给一个子线程去异步执行同时我们可以干点其他事情不用傻傻等待耗时任务执行完成。等我们的事情干完后我们再通过 Future 类获取到耗时任务的执行结果。 14. AQS
AbstractQueuedSynchronizer 抽象队列同步器用来构建锁和同步器。 15. 常见并发容器 ConcurrentHashMap : 线程安全的 HashMap CopyOnWriteArrayList : 线程安全的 List在读多写少的场合性能非常好远远好于 Vector。 ConcurrentLinkedQueue : 高效的并发队列使用链表实现。可以看做一个线程安全的 LinkedList这是一个非阻塞队列。 BlockingQueue : 这是一个接口JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列非常适合用于作为数据共享的通道。 ConcurrentSkipListMap : 跳表的实现。这是一个 Map使用跳表的数据结构进行快速查找。
##