【面试必看】Java并发

并发

1. 线程

1. 线程vs进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。 系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

线程是一个比进程更小的执行单位。 一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

比较项目进程线程
定义程序的一次执行过程,是系统运行程序的基本单位,动态的。比进程更小的执行单位,多个线程共享进程的资源。
系统中的作用系统运行一个程序即是一个进程从创建、运行到消亡的过程。一个进程在执行过程中可以产生多个线程。
资源共享各进程独立,不共享内存资源。线程共享进程的堆和方法区资源,但有自己的程序计数器、虚拟机栈和本地方法栈。
创建和切换负担系统创建和切换进程的负担较大。系统创建和切换线程的负担较小,因此线程被称为轻量级进程。
Java 中的体现启动 main 函数时启动 JVM 进程,main 函数所在的线程为主线程。线程在进程内产生,主线程和其他线程共享进程资源。

Java 程序天生就是多线程程序,一个 Java 程序的运行是 main 线程和多个其他线程同时运行

总结:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

Java 运行时数据区域(JDK1.8 之后)

一个进程中可以有多个线程。 多个线程共享进程的方法区 (元空间)。 但是每个线程有自己的程序计数器虚拟机栈本地方法栈

堆和方法区(共享)

  1. 堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),

  2. 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

程序计数器(私有)

为了线程切换后能恢复到正确的执行位置

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

虚拟机栈和本地方法栈(私有)

为了保证线程中的局部变量不被别的线程访问到

  1. 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

  2. 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

2. Java线程 vs OS线程

  • 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。

  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。

用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。

常见的三种线程模型

现在的 Java 线程的本质其实就是操作系统的线程。

3. 创建线程

使用多线程的方法:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。

真正的:new Thread().start()

4. 线程的生命周期和状态

Java 线程状态变迁图

  • NEW: 初始状态,线程被创建出来但没有被调用 start()

  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

  • BLOCKED:阻塞状态,需要等待锁释放。

  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

  • TIMED_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

  • TERMINATED:终止状态,表示该线程已经运行完毕。

  1. 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

  2. TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

  3. 当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

  4. 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

随着代码的执行在不同状态之间切换。

RUNNING vs READY

线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

RUNNABLE-VS-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,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

线程死锁示意图

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。

  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

2. 预防避免

预防:破坏死锁的产生的必要条件

  1. 破坏请求与保持条件:一次性申请所有的资源。

  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  3. 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

互斥不成立则,死锁必然不发生。(spooling假脱机技术:外围设备联机并行操作,使独占的设备变成可共享的设备)

避免:在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3.....Pn> 序列为安全序列

4. JMM(Java 内存模型)

对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。

1. CPU缓存模型

CPU 缓存则是为了解决 CPU 和内存处理速度不对等的问题。

缓存一致性协议

为了解决内存缓存不一致性问题可以通过制定缓存一致协议

2. 指令重排序

为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序,在执行代码的时候并不一定是按照你写的代码的顺序依次执行。

  • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。

  • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。

可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。

3. JMM

描述了 线程和主内存之间的关系,为共享变量提供了可见性的保障。

JMM(Java 内存模型)

线程 1 与线程 2 之间如果要进行通信:

  1. 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。

  2. 线程 2 到主存中读取对应的共享变量的值。

4. 并发编程三大特性

性质描述实现方式
原子性一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized、各种 Lock 以及各种原子类。 synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块。 各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile或者final关键字)来保证原子操作。
可见性当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值synchronizedvolatile 以及各种 Lock。将变量声明为 volatile ,指示 JVM 这个变量是共享且不稳定的,每次使用它都到主存中进行读取
有序性由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序volatile 关键字可以禁止指令进行重排序优化。

5.  volatile 关键字

1. 保证变量的可见性

修饰变量后,表示这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

JMM(Java 内存模型)

保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

2. 禁止指令重排序

防止 JVM 的指令重排序,对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

指令重排:编译器和处理器为优化执行效率而调整指令顺序的技术。它在多线程环境中可能导致并发问题,因为不同线程可能看到不一致的内存状态。通过使用volatile关键字或内存屏障,可以防止这种重排,确保程序按预期运行。

3. 不能保证原子性

利用 synchronizedLock或者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 指令

  1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。

  2. 可以避免在退出循环的时候因内存顺序冲突而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。

3. 只能保证一个共享变量的原子操作

当操作涉及跨多个共享变量时 CAS 无效。

解决:AtomicReference来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。

7. synchronized 关键字

1. what

解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

2. 使用

  1. 修饰实例方法(当前对象实例)

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

synchronized void method() {//业务代码
}

  1. 修饰静态方法(当前类)

会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

synchronized static void method() {//业务代码
}

因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。所以静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥

  1. 修饰代码块(指定类/对象)

  • 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

  1. 两者都是可重入锁

可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁

JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。

  1. synchronized -> JVM, ReentrantLock -> API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。

ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

  1. 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为 keyObject 对象为 value 的键值对。

ThreadLocal 数据结构

3. 内存泄漏

弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

ThreadLocalMap 中使用的 keyThreadLocal弱引用,而 value 强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法。

12. 线程池

1. what

管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务

2. why

为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

3. 创建

  1. ✅通过ThreadPoolExecutor构造函数来创建。

  2. ❌通过 Executor 框架的工具类 Executors 来创建。

  • FixedThreadPool固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

  • CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

  • ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。

4. 线程池的拒绝策略

当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时。

  • AbortPolicy: 抛出 RejectedExecutionException拒绝新任务的处理。

  • CallerRunsPolicy: 调用执行自己的线程运行任务。(承受此延迟并且你要求任何一个任务请求都要被执行)

  • DiscardPolicy:不处理新任务,直接丢弃掉。

  • DiscardOldestPolicy:将丢弃最早的未处理的任务请求。

5. CallerRunsPolicy 拒绝策略有什么风险?如何解决?

如果想要保证任何一个任务请求都要被执行的话,那选择 CallerRunsPolicy 拒绝策略更合适一些。

非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行,可能会内存溢出(OOM)。

解决:

  1. 暂时无法处理的任务又被保存在阻塞队列BlockingQueue中。

  2. 调整线程池的maximumPoolSize最大线程数)参数。

  3. 任务持久化

6. 线程池常见的阻塞队列

新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

  • LinkedBlockingQueue无界队列):FixedThreadPoolSingleThreadExectorFixedThreadPool最多只能创建核心线程数的线程(核心线程数和最大线程数相等),SingleThreadExector只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满,容量为 Integer.MAX_VALUE 的 。

  • SynchronousQueue(同步队列)CachedThreadPoolSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。

  • DelayedWorkQueue(延迟阻塞队列)ScheduledThreadPoolSingleThreadScheduledExecutorDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。

7. 线程池处理任务流程

图解线程池实现原理

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。

  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。

  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。

  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。

8. 线程池中的线程异常后,销毁还是复用?

  • execute()提交任务:当任务通过execute()提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。

  • submit()提交任务:对于通过submit()提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由submit()返回的Future对象中。当调用Future.get()方法时,可以捕获到一个ExecutionException。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。

使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;(不需要关注执行结果) 使用submit()时,异常被封装在Future中,线程继续复用。(更灵活的错误处理机制)

9. 其他

1. 命名

设置线程池名称前缀,有利于定位问题。

ThreadFactoryBuilder,或者自己实现 ThreadFactory

2. 线程池大小
  • 过小,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。(CPU利用不充分)

  • 过大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+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,使用跳表的数据结构进行快速查找。

##

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/17145.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ChaosMeta V0.7.0 版本发布 进入CNCF混沌工程全景图

混沌工程 ChaosMeta 的全新版本 V0.7.0 现已正式发布&#xff01;该版本包含了许多新特性和增强功能&#xff0c;在编排界面提供了多集群管理&#xff0c;在代码层面支持多命令下发通道的选择。另外由蚂蚁集团发起的ChaosMeta于北京时间2024年1月10日正式进入CNCF混沌工程全景图…

07_Servlet

Servlet 一 Servlet简介 1.1 动态资源和静态资源 静态资源 无需在程序运行时通过代码运行生成的资源,在程序运行之前就写好的资源. 例如:html css js img ,音频文件和视频文件 动态资源 需要在程序运行时通过代码运行生成的资源,在程序运行之前无法确定的数据,运行时动态生成…

转行一年了

关注、星标公众号&#xff0c;直达精彩内容 ID&#xff1a;技术让梦想更伟大 整理&#xff1a;李肖遥 来公司一年了。 说是转行其实还是在半导体行业&#xff0c;熟悉我的朋友知道 &#xff0c;我在18年开始进入半导体行业&#xff0c;那个时候想着行业很重要&#xff0c;站对了…

气泡水位计的安装方法详解(二)

气泡水位计的安装方法详解&#xff08;二&#xff09; 产品简介 气泡式水位计ZL-BWL-013是一款适用于水文、水利信息化建设领域的新一代水位测量类设备&#xff0c;产品执行GB/T 11828.2-2022标准。ZL-BWL-013气泡水位计&#xff0c;具有安装方便、易于操作&#xff0c;高精度…

算法刷题day54:搜索(一)

目录 引言一、池塘计数二、城堡问题三、山峰和山谷四、迷宫问题五、武士风度的牛六、抓住那头牛七、矩阵距离八、魔板 引言 针对于蓝桥杯&#xff0c;搜索问题还是非常之重要的&#xff0c;在省赛前深知暴搜的重要性&#xff0c;所以提前先把提高课的搜索一章给看了&#xff0…

分布式锁的原理和实现(Go)

文章目录 为什么需要分布式锁&#xff1f;go语言分布式锁的实现Redis自己的实现红锁是什么别人的带红锁的实现 etcdzk的实现 为什么需要分布式锁&#xff1f; 保证分布式系统并发请求或不同服务实例操作共享资源的安全性&#xff0c;通过一种协调机制来保证在同一时刻只有一个…

设计模式17——模板方法模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 模板方法模式&#xff08;Temp…

阿里云Linux 3.2104 LTS 64位安装SVN服务器

直接按步骤 yum install subversion 写y就行 主要是看看安装了那些文件 rpm -ql subversion 主要是为了创建版本库而准备&#xff0c;这个能一遍创建就一遍创建&#xff0c;不行就逐个创建。能创就忽略下面两个mkdir步骤。 mkdir /home/svn/groupRepos 根据新建目录作为版本…

LeetCode第131场双周赛C++题解

3158.求出出现两次数字的XOR值 给你一个数组 nums &#xff0c;数组中的数字 要么 出现一次&#xff0c;要么 出现两次。 请你返回数组中所有出现两次数字的按位 XOR 值&#xff0c;如果没有数字出现过两次&#xff0c;返回 0 。 示例 1&#xff1a; 输入&#xff1a;nums …

业务实战————Uibot6.0 .1多页面商品信息抓取RPA机器人

前言 【案例描述】 鲜果记水果店计划在淘宝电商平台上开设一家新店&#xff0c;小微是该企业运营部分的运营专员&#xff0c;主要负责公司商品上架和管理的工作。 公司计划在开店的新品促销活动中增加水果品类红富士苹果。小微需在商品上架前了解目前平台中销量前列的红富士苹…

数字水印 | 离散余弦变换 DCT 基本原理及 Python 代码实现

目录 1 基本原理2 代码实现3 图像压缩 1 基本原理 参考博客&#xff1a;https://www.cnblogs.com/zxporz/p/16072580.html D C T \mathsf{DCT} DCT 全称为 D i s c r e t e C o s i n e T r a n s f o r m \mathsf{Discrete\ Cosine\ Transform} Discrete Cosine Transfo…

新购入的读码器该如何测试呢?

物联网技术的飞速发展&#xff0c;条码二维码作为一种高效、便捷的数据传输方式&#xff0c;已经广泛应用于仓储、物流配送、零售与结算、MES系统等生活和工业领域。新购的条码二维码读码器&#xff0c;在使用前要了解它的使用方法和性能&#xff0c;以确保其性能稳定、读取准确…

小预算大效果:揭秘品牌如何用创新方法实现低成本传播

说到品牌&#xff0c;我们都知道&#xff0c;没钱是真的难搞。 品牌建设就像跑马拉松&#xff0c;得慢慢来&#xff0c;持续投入&#xff0c;一点一滴积累声誉&#xff0c;这样才能培养出忠实的粉丝团。 但别急&#xff0c;就算资金紧张&#xff0c;我们也有办法让品牌慢慢站…

基于飞书机器人跨账号消息提醒

事情的起因是飞书中不同的账号不能同时登录&#xff0c;虽然可以在飞书的账号切换页面看到其他账号下是否有消息提醒&#xff08;小红点&#xff09;&#xff0c;但是无法实现提醒功能&#xff0c;很不优雅&#xff0c;因此本文尝试提出一种新的方式实现不同账号之间的提醒功能…

自定义CSS属性(@property)解决自定义CSS变量无法实现过渡效果的问题

且看下面的代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>demot</title&g…

内存泄漏案例分享2-Fragment的内存泄漏

案例2——hprof文件显示出Fragment内存泄漏 接下来我们来看fragment内存泄漏&#xff0c;老规矩查看fields和references&#xff0c;确保它符合内存泄漏的情形&#xff1b;我们点击jump to source查看泄漏的位置 Fragment#MZBannerView#内部类Runnbale /*** Banner 切换时间间…

父进程等待子进程退出

一、 为什么要等待子进程退出&#xff1f; 等待子进程退出是为了确保父进程能够在子进程执行完毕后继续执行或者处理子进程的结果。在许多情况下&#xff0c;父进程需要等待子进程完成后才能继续执行&#xff0c;以确保正确的执行顺序和结果。 以下是一些等待子进程退出的主要…

2024年,游戏行业还值得进入吗?

来自知乎问题“2024年&#xff0c;游戏行业还值得进入吗&#xff1f;”的回答。 ——原问题描述&#xff1a;从超小厂执行策划做起&#xff0c;未来有前途吗&#xff1f; 展望2024年&#xff0c;国内外的游戏市场环境或将变得更加复杂&#xff0c;曾经那个水大鱼大的时代过去了…

C++: 二叉搜索树及实现

目录 一、二叉搜索树的概念 二、二叉搜索树的操作 2.1插入 2.2删除 1.有左子树&#xff0c;无右子树 2.有右子树&#xff0c;无左子树 3.有左子树和右子树 三、二叉搜索树的实现 要点 前言&#xff1a;为了学习map和set&#xff0c;需要先学二叉搜索树作为铺垫。 一、…

基于51单片机的函数发生器设计

一.硬件方案 此函数信号发生器是基于单片机AT89C51设计而成的&#xff0c;能够产生频率范围在0Hz—535Hz的锯齿波、正弦波、三角波、矩形波四种波形&#xff0c;并且能够通过液晶屏1602显示各自的波形类型以及频率数值。电路主要由51单片机最小系统DA0832模数转换模块运放模块…