多线程中的知识(Leo)
多线程
线程中的基础知识
线程与进程的区别
当一个程序被运行,从磁盘加载这个程序代码至内存,这时就开启了一个进程;一个线程(操作系统能够进行运算调度的最小单 位)就是一个指令流,将一条条指令以一定顺序交给CPU执行
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,当前进程下的所有线程共享内存空间
- 线程更加轻量,线程上下文切换成本一般比进程上下文切换低(上下文切换指从一个线程切换到另一个线程)线程间切换快
并行与并发的区别
定义:单核CPU下线程实际是串行的;操作系统下有个组件叫任务调度器,将cpu的时间片(windows下时间片最小约为15ms)分 给不同的程序使用,由于cpu在线程间的切换非常快,给人感觉多个线程是在同时运行的(微观串行,宏观并行);将这 种轮流使用CPU的做法叫做并发
现在都是多核CPU,在多核CPU下:
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行是同一时间执行多件事情的能力,4核CPU同时执行4个线程
线程创建的方式有哪些
1.继承Thread类,重写run方法
2.实现runnable接口,重写run方法,创建MyRun对象,创建一个Thread类的对象,并开启线程
3.实现Callable接口,重写call方法(有返回值,表示多线程运行的结果),创建MyCallable的对象(表示多线程要执行的任务),创建FutureTask的对象(作用管理多线程运行的结果),创建Thread类对象并启动
4.线程池创建线程




runnable和callable的区别
参考回答:
1. Runnable 接口run方法没有返回值
2. Callable接口call方法有返回值,是个泛型,和Future、FutureTask(作用管理多线程运行的结果)配合可以用来获取异步执行的结果
3.Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
notify()和notifyAll()有什么区别
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个wait线程
线程包括哪些状态,状态之间是怎么变化的
线程状态可参考JDK中Thread类中的枚举State:
- 新建(NEW)
- 可运行(RUNNABLE)
- 阻塞等待监视器锁的状态(BLOCKED)
- 等待(WAITING)
- 具有指定等待时间的等待线程状态(TIMED_WALTING)
- 已终止线程状态(TERMINATED)

状态之间的变化:
1.创建线程对象是新建状态;
2.调用了start()方法转变为可执行状态;
3:线程获得了CPU的执行权,执行结束是终止状态;
4:在可执行状态的过程中,如果没有获得CPU的执行权,可能会转换为其他状态:
5.如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁转换为可执行状态;
6.如果线程调用了wait()方法进入了等待状态,其他线程调用notify()唤醒后转换为可执行状态;
7.如果线程调用了sleep(50)方法进入了记时等待状态,到时间后可转换为可执行状态;
在java中wait和sleep方法的不同
共同点:wait()和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
不同点:
1.方法归属不同
- sleep(long)的方法归属是Thread的静态方法;
- wait(),wait(long)都是Object的成员方法,每个对象都有;
2.唤醒方式不同
- 执行sleep(long)和wait(long)的线程都会在等待相应ms后醒来
- wait(long)和wait()可以被notify唤醒或interrupt()中断,wait()如果不唤醒就会一直等下去
- sleep(long)无需外部通知,超时后自动恢复执行,或被
interrupt()中断。
3.锁特性不同(重点)
-
wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
-
wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃cpu,但你们还可以用)
-
sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃cpu,但你们也用不了)
4.使用场景
需要线程间协作时(如等待资源可用),使用
wait()+notify()。
仅需暂停当前线程时(如模拟延迟),使用sleep()。
5.同步块要求
-
wait()、notify()和notifyAll()必须在synchronized块或方法中调用,这是由 Java 的对象监视器 (Monitor)机制决定的。 - sleep()可以在任何地方调用
新建三个线程,如何保证它们按顺序执行
使用线程中的join方法,让当前线程等待(阻塞)调用 join() 的线程执行完毕后再继续执行

线程的run()和start()有什么区别
run():封装了要被线程执行的代码,可以被调用多次
start():用来启动线程,通过该线程调用run方法,执行run方法中所定义的逻辑代码,start方法只能被调用一次
如何停止一个正在运行的线程
有三种方式可以停止线程
1.使用退出标志,使线程正常退出,也就是当run放完成后线程终止
2.使用stop方法强行终止(不推荐,方法已经作废)
3.使用interrupt方法中断线程
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出interruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
守护线程
final void setDeamon(Boolean) 设置守护线程
当其他的非守护线程执行完毕后,守护线程会陆续结束(无论当前状态如何)
线程优先级
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
- 最大优先级是10
- 最小优先级是1
- 默认优先级是5
获取当前线程的对象
static Tread currentTread() 获取当前的线程对象
细节:
当JVM虚拟机启动后,会自动的启动多条线程,其中有一条线程就叫做main线程,它的作用是去调用main方法,并执行里面的代码
线程中并发安全
乐观锁和悲观锁思想
乐观锁:假定在操作共享资源期间不会发生冲突,所以不预先加锁;适用于读操作频繁,写操作较少的场景,因为这种场景下冲突发生的概率较低
悲观锁:假定冲突很可能发生,因此在资源访问前先加锁,以防止其他线程的干扰;适用于写操作频繁、冲突概率较高的场景
synchronized关键字的底层原理
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程再现获取这个【对象锁】时会堵塞住

底层原理:由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor;
其中monitor内部有三个属性,分别是owner,entrylist,waitset;
owner是关联的获得锁的线程,并且只能关联一个线程;
entrylist关联处于阻塞状态的线程;
waitset关联处于Waiting状态的线程;

对象的内存结构:
在HotSpot虚拟机中,对象在内存中存储的布局分为3块区域:对象头(Header),实例数据(Instance Date)和对齐填充


锁的状态升级
Monitor实现的锁属于重量级锁:
里面涉及到用户态和内核态的切换、进程的上下文切换,成本高,性能比较低
轻量级锁:
同步代码块中的代码不存在竞争,不同的线程交替使用执行同步块中的代码;这种情况使用重量级锁没必要,因此JVM(jdk1.6) 引入轻量级锁
加锁流程:
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程:
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

偏向锁:轻量级锁在没有竞争时(就自己一个线程使用),每次重入仍需执行CAS操作,java6引入偏向锁做进一步优化,只有第一次使 用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不要重新CAS,以后只要不竞 争,这个对象就归该线程所有,一旦发生竞争,都会升级为重量级锁

你谈谈JMM(Java内存模型)
JMM(Java Menory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
JMM内存分为两块,一块时私有线程的工作区域(工作内存),一块是所有内存的共享区域(主内存)
线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

CAS你知道吗
Compare And Swap(比较再交换),是一种无锁(Lock-free)的原子操作,用于在多线程环境下实现变量的线程安全更新,在无锁情况下保证线程操作共享数据的原子性
- CAS是基于乐观锁的思想;不怕别的线程来修改共享变量,就算修改了也没关系,我吃点亏再重试(失败后自旋恢复)
- syschronized是基于悲观锁的思想;最悲观的估计,得防止其他线程来修改共享变量
核心概念与原理:
CAS操作包括三个参数:内存位置(V)、预期原来值(A)和新值(B)。其原子性保证:
-
当且仅当V的值等于A时,才将V的值更新为B。
-
否则,操作失败并返回当前V的值。
整个过程由硬件层面(如CPU指令)保证原子性,无需依赖锁

优缺点:
优点
无锁性能:避免线程阻塞上下文切换,适用于短时间竞争的场景;
原子性保证:由硬件之间支持,比基于锁的实现更高效;
实现简单:简化了并发编程的逻辑(如代替sysnchronized)。
缺点
ABA问题:变量从A变成B再变回A,CAS会误认为值未变。
解决方案:使用 AtomicStampedReference 或 AtomicMarkableReference 记录版本号或标记
循环开销:若CAS长时间失败(如高竞争),会导致CPU资源浪费。
只能保证单个变量原子性:无法同时对多个变量提供原子性操作(需使用 AtomicReference 封装多个变量)
| 特性 | CAS(无锁) | 锁(如 synchronized) |
|---|---|---|
| 线程状态 | 非阻塞(自旋等待) | 阻塞(挂起线程) |
| 上下文切换 | 少(无线程挂起 / 唤醒) | 多(频繁切换用户态 / 内核态) |
| 适用场景 | 竞争少、操作短的场景 | 竞争激烈、操作长的场景 |
| 实现复杂度 | 较高(需处理 ABA、重试逻辑) | 较低(语言内置支持) |
锁的四种状态(升级顺序)
synchronized 锁的升级顺序是 无锁 → 偏向锁 → 轻量级锁 → 重量级锁,且升级过程是单向不可逆的(一旦升级为重量级锁,不会再降级为轻量级锁或偏向锁),目的是为了在 “低竞争” 和 “高竞争” 场景下都能兼顾性能。
1. 1 无锁状态(初始状态)
- 适用场景:没有线程尝试获取该锁,或线程仅执行 “读操作”(无并发修改)。
- 核心特点
- 对象头(Mark Word)中不存储锁信息,仅记录对象的哈希码、GC 分代年龄等基础信息。
- 无需任何同步开销,线程可自由访问资源。
- 示例:新建一个对象
User user = new User(),此时user作为锁载体时处于无锁状态。
1. 2 偏向锁(低竞争:仅一个线程竞争)
当只有一个线程反复获取同一把锁时,JVM 会将锁升级为 “偏向锁”—— 本质是 “偏向于第一个获取锁的线程”,避免该线程每次获取锁都需要进行 CAS 操作(轻量级锁的核心操作),进一步降低开销。
核心逻辑:
- 首次获取锁
- 线程 A 尝试获取锁时,JVM 通过 CAS 操作将对象头(Mark Word)的 “锁标志位” 设为 “偏向锁”,并记录线程 A 的 ID(存入 Mark Word)。
- 若 CAS 成功,线程 A 获得锁,后续再次获取该锁时,仅需 “检查 Mark Word 中的线程 ID 是否为自己”,无需再次 CAS,直接进入临界区。
- 锁释放
- 偏向锁不会主动释放(除非发生 “竞争” 或 “线程终止”)。线程 A 执行完临界区代码后,锁仍保持 “偏向于 A” 的状态,等待 A 再次获取。
- 竞争触发升级
- 若线程 B 尝试获取该偏向锁(此时出现竞争),JVM 会先暂停线程 A,检查 A 是否仍在执行(或已终止):
- 若 A 已终止,直接将偏向锁改为 “无锁”,让 B 重新竞争;
- 若 A 仍在执行,立即将偏向锁升级为轻量级锁,A 和 B 进入轻量级锁的竞争逻辑。
- 若线程 B 尝试获取该偏向锁(此时出现竞争),JVM 会先暂停线程 A,检查 A 是否仍在执行(或已终止):
注意:
- JDK 1.6 及以后默认开启偏向锁(可通过 JVM 参数
-XX:-UseBiasedLocking关闭)。 - 偏向锁的优势在于 “单线程重复加锁” 场景,如循环中多次调用
synchronized方法,性能提升显著。
1. 3 轻量级锁(中竞争:多个线程交替竞争)
当多个线程交替获取同一把锁(竞争不激烈,无线程长时间阻塞)时,偏向锁升级为 “轻量级锁”—— 核心是通过 CAS 操作实现锁的获取与释放,避免进入内核态(重量级锁依赖内核态)。
核心逻辑:
- 锁的获取
- 线程在进入临界区前,会先在自己的栈帧中创建一个 “锁记录(Lock Record)”,存储锁载体(对象)的 Mark Word 副本(称为 “Displaced Mark Word”)。
- 线程通过 CAS 操作,将对象头的 Mark Word 替换为 “指向自己锁记录的指针”:
- 若 CAS 成功:线程获得轻量级锁,进入临界区;
- 若 CAS 失败:说明有其他线程正在竞争(如线程 B 也尝试 CAS 修改 Mark Word),此时线程会自旋(循环重试 CAS),尝试获取锁。
- 自旋的意义
- 轻量级锁的 “自旋” 是用户态操作(无需切换到内核态),适合 “线程持有锁的时间很短” 的场景(如临界区代码执行快)。
- JVM 会限制自旋次数(默认是 10 次,或通过
-XX:PreBlockSpin调整),避免自旋时间过长导致 CPU 空耗。
- 锁的释放
- 线程执行完临界区后,通过 CAS 操作将对象头的 Mark Word 恢复为 “Displaced Mark Word”(即无锁时的状态):
- 若 CAS 成功:轻量级锁释放,其他线程可继续竞争;
- 若 CAS 失败:说明有线程在自旋等待(竞争加剧),此时会将轻量级锁升级为重量级锁。
- 线程执行完临界区后,通过 CAS 操作将对象头的 Mark Word 恢复为 “Displaced Mark Word”(即无锁时的状态):
1. 4 重量级锁(高竞争:多个线程同时阻塞)
当多个线程同时竞争锁,且自旋失败(说明竞争激烈,线程持有锁的时间较长)时,轻量级锁升级为 “重量级锁”—— 此时锁的实现依赖于 OS(操作系统)的互斥量(Mutex),线程会从 “用户态” 切换到 “内核态”,导致较大的性能开销。
核心逻辑:
- 锁的获取
- 重量级锁的对象头(Mark Word)会指向一个 “监视器锁(Monitor)”——Monitor 是 OS 层面的互斥量,维护了一个 “等待队列”(阻塞队列)。
- 线程尝试获取锁时,若 Monitor 已被占用,当前线程会直接阻塞(放弃 CPU 资源),并被放入 Monitor 的等待队列中,不再自旋。
- 锁的释放
- 持有锁的线程执行完临界区后,会释放 Monitor,并唤醒等待队列中的一个线程(公平锁按顺序唤醒,非公平锁随机唤醒),被唤醒的线程重新尝试获取 Monitor。
- 核心特点
- 开销大:线程的 “阻塞” 和 “唤醒” 需要切换内核态,耗时远大于轻量级锁的 CAS 和自旋。
- 稳定性高:适合 “高竞争、长持有” 场景(如临界区代码复杂、执行时间长),避免自旋导致的 CPU 浪费。
锁升级的核心总结(对比表)
为了更清晰地理解四种锁状态的差异,可通过以下表格对比:
| 锁状态 | 适用场景 | 实现依赖 | 性能开销 | 核心优势 |
|---|---|---|---|---|
| 无锁 | 无并发竞争 | 无 | 无 | 完全无同步开销 |
| 偏向锁 | 单线程重复加锁 | CAS(仅首次) | 极低 | 避免重复 CAS,单线程性能最优 |
| 轻量级锁 | 多线程交替竞争(短持有) | CAS + 自旋(用户态) | 较低 | 避免内核态切换 |
| 重量级锁 | 多线程同时竞争(长持有) | OS 互斥量(内核态) | 较高 | 避免 CPU 空耗,稳定处理高竞争 |
什么是AQS(AbstractQueuedSynchronizer)
- 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock(可重入锁)、Semaphore(信号量)和CountDownLatch(倒计时锁)都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部有还有一个属性state,这个state就相当于是一个资源,默认是0(无所状态),如果队列中有线程修改成功了state为1,则当前线程就相当于获取了资源
- 在对state修改的时候用啊CAS操作,保证多个线程修改情况下的原子性

公平锁和非公平锁
公平锁:
- 新的线程到队列中等待,只让队列中的head线程获取锁,先到先得(FIFO),是公平锁
- 吞吐量通常较低(频繁上下文切换),
- 响应时间更均匀
- 上下文切换较多(每次释放锁都需要唤醒队列线程)
- 饥饿风险低
- 实现:ReentrantLock(true)
非公平锁:
- 新的线程与队列中的线程共同来抢资源,可能导致后请求的线程获得锁,是非公平锁
- 吞吐量较高(减少线程挂起/唤醒)
- 可能出现极端响应时间(某些线程长期等待)
- 上下文切换较少(允许锁的连续获取)
- 饥饿风险高
- 实现:synchronized、ReentrantLock()
锁的可重入性
锁的可重入性(Reentrancy) 是指一个线程在持有某个锁的情况下,再次请求获取同一把锁时可以成功获取,而不会被自己所持有的锁阻塞。这种特性避免了线程在递归调用或重复进入同步代码块时发生死锁,是多数现代编程语言中锁机制的重要特性(如 Java 的 synchronized 和 ReentrantLock 均支持可重入)。
核心原理
可重入锁的实现依赖于计数器和线程持有者标记:
- 线程持有者标记:记录当前持有锁的线程,确保只有持有锁的线程才能再次获取锁。
- 计数器:记录线程获取锁的次数(重入次数)。每次成功获取锁时计数器 +1,释放锁时计数器 -1;当计数器减为 0 时,锁才真正被释放,其他线程可竞争。
可重入锁的优势
- 避免死锁:解决了同一线程重复获取锁时的自我阻塞问题,尤其适合递归调用场景。
- 简化编程:无需手动管理锁的嵌套获取,减少代码复杂度(例如,一个类的多个同步方法相互调用时无需额外处理)。
- 符合直觉:开发者无需担心 “自己持有锁却无法再次进入同步代码” 的问题。
ReentrantLock的实现原理
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断
- 支持公平锁和非公平锁,在提供构造器中参数(true/false),syschronized仅支持非公平锁
- ReentrantLock主要利用CAS+AQS队列来实现
- 可以设置超时时间
- 支持多个条件变量
- 与synchronized一样,可支持重入


synchronized和Lock有什么区别
语法层面:
synchronized是关键字,源码在jvm中,由c++实现
Lock是接口,源码由jdk提供,用java语言实现
使用synchroized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动释放
功能层面:
二者均属于悲观锁,都具备互斥、同步、锁重入功能
Lock提供许多synchroized不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)
性能层面:
在没有竞争时,synchroized做了许多优化,如轻量锁,偏向锁,性能不赖
在竞争激烈时,Lock的实现通常会提供更好的性能
死锁的产生条件是什么
死锁是指两个或多个线程相互持有对方所需的资源,且都不主动释放,导致所有线程永远阻塞的状态。
死锁的产生必须同时满足互斥、持有并等待、不可剥夺、循环等待四个条件。
如果进行死锁诊断
- 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁
-
如果有死锁现象,需要查看具体代码分析后,可修复
-
- 可视化工具jconsole、VisualVM也可以检查死锁问题
谈谈对volatile的理解
1.保证线程间的可见性
用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个变量可见
2.禁止进行指令重排序
用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,防止其他读写操作越过屏障,从而到达重排序的效果
聊一下ConcurrentHashMap
底层数据结构:
JDK1.7底层采用分段的数组+链表实现
JDK8后采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
加锁的方式:
JDK1.7采用Segment分段锁,底层采用的是ReentrantLock
JDK8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对jdk1.7的颗粒度更细,性能更好
导致并发程序出现问题的根本原因是什么
Java并发编程的三大特性:原子性,可见性和有序性
原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行(用synchronized/Lock解决)
内存可见性:让一个线程对共享变量的修改对另一个线程可见(volatile解决)
有序性:处理器为了提高程序运行效率,可能会对输入代码进行优化:不保证语句的执行先后,但保证最终执行结果 (volatile解决)
线程池
说一下线程池的核心参数(线程池的执行原理知道吗)
核心线程数(corePoolSize):
线程池长期维持的线程数量,即使线程处于空闲状态,也不会销毁(除非设置了allowCoreThreadTimeOut)。
最大线程数(maximumPoolSize):
- 线程池允许的最大线程数量
- 当任务队列已满且未达到最大线程时,线程池会创建新的临时线程来处理任务。
线程存活时间(keepAliveTime):
- 当前线程数超过核心线程数时,多余的空闲线程在被销毁前等待新任务的最长时间
- 若设置了allowCoreThredTime为true,则核心线程也会受此参数影响
时间单位(TimeUnit):
KeepAliveTime的时间单位,如TimeUnitSECOND;
任务队列(workQueue):
用于存储待执行任务的阻塞队列,常见类型包括:
ArryBlockingQueue:有界队列,需指定容量
LinkedBlockingQueue:无界队列(默认容量为Integer.max_value),可能导致OOM。
synchronousQueue:不存储任务,每个插入操作必须等待另一个线程的移除操作。
PriorityBlockingQueue:优先级队列,按任务优先级排序。
线程工厂(ThreadFactory):
-
用于创建线程的工厂类,可自定义线程名称、优先级等属性。 -
通常使用`Executors.defaultThreadFactory()`或自定义实现。
拒绝策略(RejectedExecutionHandler):
- 当任务队列已满且线程数达到最大线程数时,新提交任务的处理策略。
- 常见策略包括:
AbortPolicy(默认):抛出RejectedExecutionException异常。CallerRunsPolicy:由提交任务的线程直接执行。DiscardPolicy:直接丢弃任务,不做任何处理。DiscardOldestPolicy:丢弃队列中最老的任务,尝试提交新任务。

线程池的状态
- Running:表示线程池正常运行,既能接受新任务,也会正常处理队列中的任务
Shutdown:当调用线程池的shutdown()方法时,线程池就进入ShutDown状态,表示线程池处于正在关闭状态,此状态下线程池不会接受新任务,但是会继续把队列中的任务处理完 - Stop:当调用线程池的shutdownnow()方法时,线程池就进入STOP状态,表示线程池处于正在停止状态,此状态下线程池既不会接受新任务了,也不会处理队列中的任务,并且正在运行的线程也会中断
- Tidying:线程池中没有线程在运行后,线程池的状态就会变成Tidying,Shutdown完成后,并且会调用terminated(),该方法是空方法,留给程序员进行扩展
- Terminated:terminated()方法执行完之后,线程池状态就会变成terminnate
线程池中有哪些常见的阻塞队列
workQueue--当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO
2.LinkedBlockingQueue:基于链表结构的无界阻塞队列(默认无界),FIFO
3.DeleayedWorkQueue:是一个优先级队列,保证每次出任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的队列,每次插入操作都必须等待一个移除操作
ArrayBlockingQueue和LinkedBlockingQueue的区别:

如何确定核心线程数
参考回答:
① 高并发、任务执行时间短 ->( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
IO密集型的任务 ->(CPU核数 * 2 + 1)
计算密集型任务 ->( CPU核数+1 )
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据 是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)
线程池的种类有哪些
①newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
②newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照 指定顺序(FIFO)执行(严格)
③newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收, 则新建线程
④ewScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
为什么不建议用Executors创建线程池

当我们使用Executors创建FixedThreadPool/SingleThreadPool时,对应构造方法创建的队列为LinkedBlockingQueue,是一个无界队列,如果使用该线程池执行任务,任务越多占用的内存越多,最终可能耗尽内存,导致OOM;

使用场景
线程池使用场景(你们项目中哪里用到了线程池)

批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM(内存溢出)
数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间
如何控制某个方法允许并发访问线程的数量
在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量
1.创建Semaphore对象,可以给一个容量
2.acquire()可以请求一个信号量,这时候的信号量个数-1
3.release()释放一个信号量,此时信号量个数+1
谈谈你对ThreadLocal的理解
ThreadLocal是 Java 中的一个线程局部变量工具类,用于为每个使用该变量的线程都创建一个独立的副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。以下是关于ThreadLocal的详细解析:
核心概念
- 线程隔离
ThreadLocal为每个线程提供独立的变量副本,每个线程都可以独立修改自己的副本,线程间互不影响。 - 存储结构
- 每个
Thread对象内部都有一个ThreadLocalMap,用于存储该线程的所有局部变量。 ThreadLocalMap的键为ThreadLocal对象的弱引用,值为线程局部变量的副本。
- 每个
- 弱引用机制
- 当外部没有强引用指向
ThreadLocal对象时,其弱引用会被垃圾回收(GC),避免内存泄漏。 - 但如果线程一直存活(如线程池中的线程),对应的
ThreadLocalMap中的值可能不会被回收,需手动调用``。
- 当外部没有强引用指向
常用方法
T get():获取当前线程的局部变量副本。void set(T value):设置当前线程的局部变量副本。void remove():移除当前线程的局部变量副本,防止内存泄漏。T initialValue():返回初始值(需通过继承ThreadLocal并重写该方法)。
与同步机制的对比
| 特性 | ThreadLocal | 同步机制(如synchronized) |
|---|---|---|
| 数据所有权 | 每个线程独立拥有副本 | 共享同一个实例 |
| 线程安全方式 | 空间换时间(避免竞争) | 时间换空间(通过锁序列化访问) |
| 适用场景 | 状态不共享的场景(如用户会话) | 状态必须共享的场景(如计数器) |
注意事项
- 不可继承性:子线程无法访问父线程的
ThreadLocal值,如需继承可使用InheritableThreadLocal。 - 性能开销:频繁创建和销毁
ThreadLocal对象可能影响性能,建议复用。 - 线程池场景:线程池中的线程会被复用,需特别注意
ThreadLocal的清理。
总结
ThreadLocal通过线程隔离的方式,为每个线程提供独立的变量副本,解决了多线程环境下的状态共享问题。但需警惕内存泄漏风险,合理使用remove()方法,并结合业务场景评估其适用性。
ThreadLocal的内存泄漏问题
Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用
强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它,即使堆内存不足,宁可OOM,也不会回收

弱引用:表示一个对象处于可能有用且非必须。在GC线程扫描内存区域时,一旦发现弱引用,就会回收弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量的副本

浙公网安备 33010602011771号