线程相关面试题
提示:线程相关面试题,持续更新中
文章目录
- 一、Java线程池
- 1、Java线程池有哪些核心参数,分别有什么的作用?
- 2、线程池有哪些拒绝策略?
- 3、说一说线程池的执行流程?
- 4、线程池核心线程数怎么设置呢?
- 4、Java线程池中submit()和execute()方法有什么区别?
- 二、ThreadLocal
- 1、请介绍一下ThreadLocal底层是怎么实现的?
- 2、ThreadLocal为什么会内存泄漏?
- 三、Thread
- 1、请说说sleep()和wait()有什么区别?
- 2、多个线程如何保证按顺序执行?
一、Java线程池
1、Java线程池有哪些核心参数,分别有什么的作用?
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8,16,60,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(1024),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy()
);构造方法最多的是7个参数:(1)int corePoolSize : 线程池中的核心线程数量allowCoreThreadTimeOut:允许核心线程超时销毁,默认false(不销毁);boolean prestartCoreThread(),初始化一个核心线程;int prestartAllCoreThreads(),初始化所有核心线程;(2)int maximumPoolSize : 线程池中允许的最大线程数当核心线程全部繁忙且任务队列存满之后,线程池会临时追加线程,直到总线程数达到maximumPoolSize这个上限;(3)long keepAliveTime :线程空闲超时时间如果一个非核心线程处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁;(4)TimeUnit unit :keepAliveTime的时间单位(天、小时、分、秒......);(5)BlockingQueue<Runnable> workQueue :任务队列当核心线程全部繁忙时,任务存放到该任务队列中,等待被核心线程来执行;(6)ThreadFactory threadFactory :线程工厂用于创建线程,一般采用默认的线程工厂即可,也可以自定义实现;Executors.defaultThreadFactory():推荐使用,Executors.privilegedThreadFactory():已过时,不推荐使用,(7)RejectedExecutionHandler handler :拒绝策略(饱和策略)当任务太多来不及处理时,如何“拒绝”任务?“拒绝”触发条件:1、核心线程corePoolSize正在执行任务;2、线程池的任务队列workQueue已满;3、线程池中的线程数达到maximumPoolSize时;就需要“拒绝”掉新提交过来的任务;
2、线程池有哪些拒绝策略?
JDK提供了4种内置的拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy和DiscardPolicy;1、AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常,这是默认的拒绝策略;2、DiscardPolicy:直接丢弃任务,不抛出异常,没有任何提示;3、DiscardOldestPolicy:丢弃任务队列中靠最前的任务,当前提交的任务不会丢弃;4、CallerRunsPolicy: 交由任务的调用线程(提交任务的线程)来执行当前任务;除了上面的四种拒绝策略,还可以通过实现RejectedExecutionHandler接口,实现自定义的拒绝策略;
3、说一说线程池的执行流程?
当提交一个新任务到线程池时,具体的执行流程如下: 1. 当我们提交任务,线程池会根据corePoolSize大小创建线程来执行任务; 2. 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队;3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize个额外创建的线程等待 keepAliveTime之后被自动销毁;4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略进行拒绝处理;
4、线程池核心线程数怎么设置呢?
Ncpu = cpu的核心数 ,Ucpu = cpu的使用率(在0~1之间)W = 线程等待时间,C = 线程计算时间举例:
8 * 100% * (1+60/40) = 20
8 * 100% * (1+80/20) = 40----------------------------------------------------------------------------------------------------------------------------任务分为CPU密集型和IO密集型 CPU密集型线程数 = CPU核心数 + 1; 这种任务主要是消耗CPU资源, 比如像加解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务;+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间; IO密集型线程数 = CPU核心数 * 2;这种任务会有大部分时间在进行IO操作,比如像MySQL数据库、文件读写、网络通信等任务,这类任务不会特别消耗CPU资源,但是IO操作比较耗时,会占用比较多时间;线程在处理IO的时间段内不会占用CPU,这时就可以将CPU交出给其它线程使用,因此在IO密集型任务的应用中,可以多配置一些线程; ----------------------------------------------------------------------------------------------------------------------------基本原则:1、线程执行时间越多,就需要越少的线程;2、线程等待时间越多,就需要越多的线程;以上理论参考依据,实际项目中建议在本地或者测试环境进行压测多次调整线程池大小,找到相对理想的值大小;
4、Java线程池中submit()和execute()方法有什么区别?
1、 两个方法都可以向线程池提交任务;2、 execute只能提交Runnable,无返回值;3、 submit既可以提交Runnable,返回值为null,也可以提交Callable,返回值Future;4、 execute()方法定义在Executor接口中;5、 submit()方法定义在ExecutorService接口中;6、 execute执行任务时遇到异常会直接抛出;7、 submit执行任务时遇到异常不会直接抛出,只有在调用Future的get()方法获取返回值时,才会抛出异常;-----------------------------------------------------------------------------------------------------------------------Runnable和Callable都是用于定义线程执行任务的接口特性 Runnable Callable返回值 无(void) 有(泛型V)异常处理 不能抛出受检异常 可以抛出受检异常方法签名 void run() V call() throws Exception使用场景 简单任务,无需结果 复杂任务,需要结果或异常处理线程池集成 execute(Runnable) submit(Callable) + Future功能扩展性 基础 支持任务取消、超时、结果获取选择建议使用Runnable:当任务简单、无需返回值且不涉及受检异常时。使用Callable:当任务需要返回值、可能抛出异常或需要高级控制(如超时、取消)时。
二、ThreadLocal
1、请介绍一下ThreadLocal底层是怎么实现的?
定义:ThreadLocal 是 Java 中用于实现线程隔离存储的核心工具类,它为每个线程提供独立的变量副本,确保线程间数据互不干扰。-------------------------------------------------------------------------------------------------------------------------ThreadLocal是一个类似于HashMap的数据结构;ThreadLocal的实现原理就是通过set把value set到线程的threadlocals属性中,threadlocals是一个Map,其中的key是ThreadLocal的this引用,value是我们所set的值;-------------------------------------------------------------------------------------------------------------------------解释:1、线程运行时,会生成ThreadLocalMap类型的名称为threadLocals的成员变量2、ThreadLocalMap底层是Entry 数组键值对存储:Entry[] 数组存储键值对,键为 ThreadLocal 对象,值为线程本地变量。弱引用键:Entry 的键是弱引用(WeakReference<ThreadLocal<?>>),避免 ThreadLocal 实例无法被 GC 回收。但值仍为强引用,需手动清理。
2、ThreadLocal为什么会内存泄漏?
一、内存泄漏的核心原因1、弱引用键(Weak Key)的特性:ThreadLocalMap 的 Entry 键是弱引用类型,指向 ThreadLocal 对象。弱引用的特点:当 ThreadLocal 实例没有其他强引用时,GC 会回收它,但 Entry 的值(Value)仍被强引用持有。2、值对象的强引用(Strong Value):Entry 的值是强引用,指向用户存储的对象。问题:即使 ThreadLocal 实例被 GC 回收,Entry 的值仍可能被线程长期持有(如线程池中的线程),导致值对象无法被释放。二、内存泄漏的典型场景1、线程长期存活:线程池场景:线程被复用(如 ExecutorService),且未调用 ThreadLocal.remove()。后果:Entry 的值对象一直被线程持有,无法被 GC 回收。2、未调用 remove() 方法:代码疏忽:在使用完 ThreadLocal 后,未显式调用 remove() 清理值。后果:即使 ThreadLocal 实例被回收,值对象仍可能残留在 ThreadLocalMap 中。三、内存泄漏的后果1、内存占用增长:值对象无法被释放,导致堆内存占用持续增加。2、OOM 风险:在极端情况下,可能引发 OutOfMemoryError(OOM)。四、解决方案与最佳实践1、显式调用 remove():必须执行:在使用完 ThreadLocal 后,务必调用 remove() 清理值。示例:try {threadLocal.set(value);// ... 业务逻辑 ...} finally {threadLocal.remove(); // 确保清理}2、避免静态 ThreadLocal 变量:风险:静态变量生命周期长,可能导致 ThreadLocalMap 长期存活。替代方案:使用局部变量或依赖注入框架管理。3、线程池场景的特殊处理:自定义线程池:在任务执行前后清理 ThreadLocal。使用 ThreadLocal 清理工具:如阿里开源的 TransmittableThreadLocal,支持线程池传递和清理。4、依赖 GC 的局限性:不要依赖 GC:虽然 ThreadLocalMap 在 set/get 操作时会扫描并清理部分过期 Entry(expungeStaleEntry),但这种清理是启发式的,无法保证完全释放。五、底层机制补充1、ThreadLocalMap 的清理逻辑:探测式清理:在 set/get 操作时,扫描 Entry 数组,发现键为 null 的 Entry,清除值并释放键。启发式清理:以对数复杂度清理部分过期数据,平衡性能与内存。2、为什么值不使用弱引用?设计权衡:若值也使用弱引用,可能导致用户无意识丢失数据。替代方案:通过 remove() 显式管理值的生命周期。
三、Thread
1、请说说sleep()和wait()有什么区别?
1. 所属类与方法签名sleep():属于Thread类。方法签名:public static void sleep(long millis) throws InterruptedException。是一个静态方法,作用于当前线程。wait():属于Object类。方法签名:public final void wait() throws InterruptedException(还有其他重载方法,如wait(long timeout))。是一个实例方法,必须通过对象调用。2. 核心作用sleep():让当前线程暂停执行一段时间(以毫秒为单位)。不释放锁:线程在睡眠期间仍持有对象锁或监视器锁。用途:模拟延时、暂停线程执行(如轮询间隔、超时控制)。wait():让当前线程等待,释放对象锁,直到其他线程调用notify()或notifyAll()。释放锁:线程在等待期间不再持有对象锁。用途:多线程间协调,等待某个条件满足(如生产者-消费者模型)。3. 线程状态变化sleep():线程进入TIMED_WAITING状态。时间到后自动恢复执行。wait():线程进入WAITING(无超时)或TIMED_WAITING(有超时)状态。必须由其他线程调用notify()/notifyAll()(或超时)才能恢复。4. 锁的行为sleep():不释放锁:即使线程睡眠,其他线程仍无法获取该线程持有的锁。wait():释放锁:线程释放对象锁,允许其他线程竞争锁并执行同步代码块。5. 唤醒机制sleep():自动唤醒:睡眠时间到后,线程自动恢复执行。wait():被动唤醒:必须由其他线程调用notify()(唤醒单个等待线程)或notifyAll()(唤醒所有等待线程)。6. 使用场景sleep():简单延时(如定时任务、超时重试)。示例:try {Thread.sleep(1000); // 睡眠1秒} catch (InterruptedException e) {e.printStackTrace();}wait():多线程协作(如生产者-消费者模型)。示例:synchronized (lock) {while (!condition) {lock.wait(); // 等待条件满足}// 执行操作}
特性 | sleep() | wait() |
---|---|---|
所属类 | Thread | Object |
释放锁 | 否 | 是 |
唤醒方式 | 自动(时间到) | 被动(notify()/notifyAll()) |
线程状态 | TIMED_WAITING | WAITING/TIMED_WAITING |
典型用途 | 简单延时 | 多线程协作 |
是否静态方法 | 是 | 否(需通过对象调用) |
2、多个线程如何保证按顺序执行?
比如任务B,它需要等待任务A执行之后才能执行,任务C需要等待任务B执行之后才能执行;解决方法:1、通过join()方法使当前线程“阻塞”,等待指定线程执行完毕后继续执行;2、通过创建单一化线程池newSingleThreadExecutor()实现;3、通过倒数计时器CountDownLatch实现;4、使用Object的wait/notify方法实现;5、使用线程的Condition(条件变量)方法实现;6、使用线程的CyclicBarrier(回环栅栏)方法实现;7、使用线程的Semaphore(信号量)方法实现;
(1) 通过join()方法使当前线程“阻塞”,等待指定线程执行完毕后继续执行:
方法1:public static void main(String[] args) throws Exception {// t1线程Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 执行");}, "t1");// t2线程Thread t2 = new Thread(() -> {try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 执行");}, "t2");// t3线程Thread t3 = new Thread(() -> {try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 执行");}, "t3");t1.start();t2.start();t3.start();}---------------------------------------------------------------------------------------------------------------------------
方法2:public static void main(String[] args) throws Exception {// t1线程Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 执行");}, "t1");// t2线程Thread t2 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 执行");}, "t2");// t3线程Thread t3 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 执行");}, "t3");t1.start();t1.join();t2.start();t2.join();t3.start();}
(2) 通过创建单一化线程池newSingleThreadExecutor()实现:
public class Test {//自己手动创建单一化线程池(推荐使用)private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy());//创建单一化线程池(不建议使用Executors)private static ExecutorService executorService = Executors.newSingleThreadExecutor();public static void main(String[] args) throws Exception {// t1线程Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " t1执行");}, "t1");// t2线程Thread t2 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " t2执行");}, "t2");// t3线程Thread t3 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " t3执行");}, "t3");threadPoolExecutor.execute(t1);threadPoolExecutor.execute(t2);threadPoolExecutor.execute(t3);threadPoolExecutor.shutdown();}
}
(3) 通过倒数计时器CountDownLatch实现:
public class Test {/** 用于判断t1线程是否执行,倒计时设置为1,执行后减1 */private static final CountDownLatch countDownLatch1 = new CountDownLatch(1);/** 用于判断t2线程是否执行,倒计时设置为1,执行后减1 */private static final CountDownLatch countDownLatch2 = new CountDownLatch(1);public static void main(String[] args) throws Exception {// t1线程Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 执行");countDownLatch1.countDown(); // -1}, "t1");// t2线程Thread t2 = new Thread(() -> {try {countDownLatch1.await(); //只有当 countDownLatch1 的计数器归零后,才会继续执行后续代码} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 执行");countDownLatch2.countDown();}, "t2");// t3线程Thread t3 = new Thread(() -> {try {countDownLatch2.await(); //只有当 countDownLatch2 的计数器归零后,才会继续执行后续代码} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 执行");}, "t3");t1.start();t2.start();t3.start();}
}