欢迎关注方志朋的博客,回复”666“获面试宝典

前几天,在帮同事排查一个线上偶发的线程池错误

逻辑很简单,线程池执行了一个带结果的异步任务。但是最近有偶发的报错:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@a5acd19 rejected from java.util.concurrent.ThreadPoolExecutor@30890a38[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]

本文中的模拟代码已经问题都是在HotSpot java8 (1.8.0_221)版本下模拟&出现的

下面是模拟代码,通过Executors.newSingleThreadExecutor创建一个单线程的线程池,然后在调用方获取Future的结果

public class ThreadPoolTest {public static void main(String[] args) {final ThreadPoolTest threadPoolTest = new ThreadPoolTest();for (int i = 0; i < 8; i++) {new Thread(new Runnable() {@Overridepublic void run() {while (true) {Future<String> future = threadPoolTest.submit();try {String s = future.get();} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();} catch (Error e) {e.printStackTrace();}}}}).start();}//子线程不停gc,模拟偶发的gcnew Thread(new Runnable() {@Overridepublic void run() {while (true) {System.gc();}}}).start();}/*** 异步执行任务* @return*/public Future<String> submit() {//关键点,通过Executors.newSingleThreadExecutor创建一个单线程的线程池ExecutorService executorService = Executors.newSingleThreadExecutor();FutureTask<String> futureTask = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception {Thread.sleep(50);return System.currentTimeMillis() + "";}});executorService.execute(futureTask);return futureTask;}
}

分析&疑问

第一个思考的问题是:线程池为什么关闭了,代码中并没有手动关闭的地方。看一下 Executors.newSingleThreadExecotor的源码实现:

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

这里创建的实际上是一个 FinalizableDelegatedExecutorService,这个包装类重写了 finalize函数,也就是说这个类会在被GC回收之前,先执行线程池的shutdown方法。

问题来了,GC只会回收不可达(unreachable)的对象,在 submit函数的栈帧未执行完出栈之前, executorService应该是可达的才对。

对于此问题,先抛出结论:

当对象仍存在于作用域(stack frame)时, finalize也可能会被执行

oracle jdk文档中有一段关于finalize的介绍:

https://docs.oracle.com/javas...

A reachable object is any object that can be accessed in any potential continuing computation from any live thread.

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.

大概意思是:可达对象(reachable object)是可以从任何活动线程的任何潜在的持续访问中的任何对象;java编译器或代码生成器可能会对不再访问的对象提前置为null,使得对象可以被提前回收

也就是说,在jvm的优化下,可能会出现对象不可达之后被提前置空并回收的情况

举个例子来验证一下(摘自https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope):

class A {@Override protected void finalize() {System.out.println(this + " was finalized!");}public static void main(String[] args) throws InterruptedException {A a = new A();System.out.println("Created " + a);for (int i = 0; i < 1_000_000_000; i++) {if (i % 1_000_00 == 0)System.gc();}System.out.println("done.");}
}
//打印结果
Created A@1be6f5c3
A@1be6f5c3 was finalized!//finalize方法输出
done.

从例子中可以看到,如果a在循环完成后已经不再使用了,则会出现先执行finalize的情况;虽然从对象作用域来说,方法没有执行完,栈帧并没有出栈,但是还是会被提前执行。

现在来增加一行代码,在最后一行打印对象a,让编译器/代码生成器认为后面有对象a的引用

...
System.out.println(a);
//打印结果
Created A@1be6f5c3
done.
A@1be6f5c3

从结果上看,finalize方法都没有执行(因为main方法执行完成后进程直接结束了),更不会出现提前finalize的问题了

基于上面的测试结果,再测试一种情况,在循环之前先将对象a置为null,并且在最后打印保持对象a的引用

A a = new A();
System.out.println("Created " + a);
a = null;//手动置null
for (int i = 0; i < 1_000_000_000; i++) {if (i % 1_000_00 == 0)System.gc();
}
System.out.println("done.");
System.out.println(a);
//打印结果
Created A@1be6f5c3
A@1be6f5c3 was finalized!
done.
null

从结果上看,手动置null的话也会导致对象被提前回收,虽然在最后还有引用,但此时引用的也是null了


现在再回到上面的线程池问题,根据上面介绍的机制,在分析没有引用之后,对象会被提前finalize

可在上述代码中,return之前明明是有引用的 executorService.execute(futureTask),为什么也会提前finalize呢?

猜测可能是由于在execute方法中,会调用threadPoolExecutor,会创建并启动一个新线程,这时会发生一次主动的线程切换,导致在活动线程中对象不可达

结合上面Oracle Jdk文档中的描述“可达对象(reachable object)是可以从任何活动线程的任何潜在的持续访问中的任何对象”,可以认为可能是因为一次显示的线程切换,对象被认为不可达了,导致线程池被提前finalize了

下面来验证一下猜想:

//入口函数
public class FinalizedTest {public static void main(String[] args) {final FinalizedTest finalizedTest = new FinalizedTest();for (int i = 0; i < 8; i++) {new Thread(new Runnable() {@Overridepublic void run() {while (true) {TFutureTask future = finalizedTest.submit();}}}).start();}new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.gc();}}}).start();}public TFutureTask submit(){TExecutorService TExecutorService = Executors.create();TExecutorService.execute();return null;}
}
//Executors.java,模拟juc的Executors
public class Executors {/*** 模拟Executors.createSingleExecutor* @return*/public static TExecutorService create(){return new FinalizableDelegatedTExecutorService(new TThreadPoolExecutor());}static class FinalizableDelegatedTExecutorService extends DelegatedTExecutorService {FinalizableDelegatedTExecutorService(TExecutorService executor) {super(executor);}/*** 析构函数中执行shutdown,修改线程池状态* @throws Throwable*/@Overrideprotected void finalize() throws Throwable {super.shutdown();}}static class DelegatedTExecutorService extends TExecutorService {protected TExecutorService e;public DelegatedTExecutorService(TExecutorService executor) {this.e = executor;}@Overridepublic void execute() {e.execute();}@Overridepublic void shutdown() {e.shutdown();}}
}
//TThreadPoolExecutor.java,模拟juc的ThreadPoolExecutor
public class TThreadPoolExecutor extends TExecutorService {/*** 线程池状态,false:未关闭,true已关闭*/private AtomicBoolean ctl = new AtomicBoolean();@Overridepublic void execute() {//启动一个新线程,模拟ThreadPoolExecutor.executenew Thread(new Runnable() {@Overridepublic void run() {}}).start();//模拟ThreadPoolExecutor,启动新建线程后,循环检查线程池状态,验证是否会在finalize中shutdown//如果线程池被提前shutdown,则抛出异常for (int i = 0; i < 1_000_000; i++) {if(ctl.get()){throw new RuntimeException("reject!!!["+ctl.get()+"]");}}}@Overridepublic void shutdown() {ctl.compareAndSet(false,true);}
}

执行若干时间后报错:

Exception in thread "Thread-1" java.lang.RuntimeException: reject!!![true]

从错误上来看,“线程池”同样被提前shutdown了,那么一定是由于新建线程导致的吗?

下面将新建线程修改为 Thread.sleep测试一下:

//TThreadPoolExecutor.java,修改后的execute方法
public void execute() {try {//显式的sleep 1 ns,主动切换线程TimeUnit.NANOSECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//模拟ThreadPoolExecutor,启动新建线程后,循环检查线程池状态,验证是否会在finalize中shutdown//如果线程池被提前shutdown,则抛出异常for (int i = 0; i < 1_000_000; i++) {if(ctl.get()){throw new RuntimeException("reject!!!["+ctl.get()+"]");}}
}

执行结果一样是报错

Exception in thread "Thread-3" java.lang.RuntimeException: reject!!![true]

由此可得,如果在执行的过程中,发生一次显式的线程切换,则会让编译器/代码生成器认为外层包装对象不可达

总结

虽然GC只会回收不可达GC ROOT的对象,但是在编译器(没有明确指出,也可能是JIT)/代码生成器的优化下,可能会出现对象提前置null,或者线程切换导致的“提前对象不可达”的情况。

所以如果想在finalize方法里做些事情的话,一定在最后显示的引用一下对象(toString/hashcode都可以),保持对象的可达性(reachable)

上面关于线程切换导致的对象不可达,没有官方文献的支持,只是个人一个测试结果,如有问题欢迎指出

综上所述,这种回收机制并不是JDK的bug,而算是一个优化策略,提前回收而已;但 Executors.newSingleThreadExecutor的实现里通过finalize来自动关闭线程池的做法是有Bug的,在经过优化后可能会导致线程池的提前shutdown,从而导致异常。

线程池的这个问题,在JDK的论坛里也是一个公开但未解决状态的问题https://bugs.openjdk.java.net/browse/JDK-8145304。

不过在JDK11下,该问题已经被修复:

JUC  Executors.FinalizableDelegatedExecutorService
public void execute(Runnable command) {try {e.execute(command);} finally { reachabilityFence(this); }
}

来源:https://urlify.cn/63QrYv

热门内容:
  • 牛逼,国产开源的远程桌面火了,只有9MB,支持自建中继器!

  • 吃透这“ 16个 ”核心技术栈,月薪3W随便叫!

  • 不满月薪12000辞职,跳槽直接进大厂,据说背了很多面试八股文?!

  • 再见了月薪3w的大后端,低代码开发已成气候!

  • 今天面了个阿里出来的大佬,见识到了基础天花板!

  • 发现一款好用到爆的数据库工具,被惊艳到了!

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

线程池的一个BUG,被我发现了相关推荐

  1. 记一次线程池引发的BUG,差点被祭天

    点赞再看,养成习惯,微信搜索[敖丙]关注这个互联网苟且偷生的工具人. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文 ...

  2. 随笔之如何实现一个线程池

    为什么80%的码农都做不了架构师?>>>    一 缘由:     最近因工作问题,需要实现一个简单的线程池,满足一下要求, 可伸缩,即一旦发现线程不够用,则可以动态增加线程.(至于 ...

  3. 由于不知线程池的bug,某Java程序员叕被祭天

    说说你对线程池的理解? 首先明确,池化的意义在于缓存,创建性能开销较大的对象,比如线程池.连接池.内存池.预先在池里创建一些对象,使用时直接取,用完就归还复用,使用策略调整池中缓存对象的数量. Jav ...

  4. 程序员修仙之路--设计一个实用的线程池

    菜菜呀,我最近研究技术呢,发现线上一个任务程序线程数有点多呀 CEO,CTO,CFO于一身的CXO x总,你学编程呢? 菜菜 作为公司总负责人,我以后还要管理技术部门呢,怎么能不会技术呢 CEO,CT ...

  5. Linux下设计一个简单的线程池

    定义 什么是线程池?简单点说,线程池就是有一堆已经创建好了的线程,初始它们都处于空闲等待状态,当有新的任务需要处理的时候,就从这个池子里面取一个空闲等待的线程来处理该任务,当处理完成了就再次把该线程放 ...

  6. 面试官:如何评估一个线程池需要设置多少个线程

    作者 | 丁威       责编 | 欧阳姝黎 见字如面,我是威哥,一个从普通二本院校毕业,从未曾接触分布式.微服务.高并发到通过技术分享实现职场蜕变,成长为 RocketMQ 社区优秀布道师.大厂资 ...

  7. 使用VC实现一个“智能”自增减线程池

    工作中接手了一款产品的改造.因为该产品可能使用很多线程,所以产品中使用了线程池.(转载请指明来自BreakSoftware的CSDN博客) 线程池的一个优点是降低线程创建和销毁的频率:缺点是可能在比较 ...

  8. 用Python实现一个简单的线程池

    线程池的概念是什么? 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是 如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收 ...

  9. C++实现一个简易的线程池

    文章目录 线程池的概念 什么是线程池 线程池的优点 线程池的应用场景 线程池的实现 实现思路 代码实现 线程池的概念 什么是线程池 顾名思义,线程池就是一个有很多空闲线程的池子(线程的数量受到限制), ...

最新文章

  1. 降维打击:这款GAN可以让真人「二次元化」
  2. 重置忘记的mysql root密码
  3. netstat -an中state含义
  4. c语言用hash方式数组去重,js数组去重的hash方法
  5. MySQL优化CPU消耗
  6. 移动云TeaTalk(长沙站)| 聚焦“云网一体”发展新机遇
  7. 5分钟掌握var,let和const异同
  8. [置顶] Android adb root权限
  9. 引用、取址运算符、解引用运算符——傻傻分不清楚
  10. html中logo不变形,CSS3如何实现LOGO中的文本变形动画
  11. 会计计算机论文,关于计算机论文格式模板,关于电算化会计相关论文范文资料...
  12. LM2596、LM2576
  13. mysql生成随机姓名、手机号、日期
  14. 绘制相同到期日欧式期权组合收益图(python)
  15. 从实践角度重新理解BIO和NIO
  16. python爬虫爬取深交所数据
  17. 如何用CSS实现角标
  18. 解决 Linux 系统,出现“不在sudoers文件中,此事将被报告”的问题
  19. 母函数 By Tanky Woo
  20. Springboot基础(二):数据库+Thymeleaf

热门文章

  1. 程序性能监控分析工具
  2. springboot 事务手动回滚_来,讲讲Spring事务有哪些坑?
  3. 2018-3-10论文(网络评论非结构化信息表示与应用研究)笔记-----基于证据理论的综合评价模型建立
  4. jvm七种垃圾收集器
  5. [Git/Github] ubuntu 14.0 下github 配置
  6. jquery radio 取值
  7. 关于Silverlight中多项目共享DLL文件的讨论
  8. Flex精华摘要--使用AS脚本
  9. 从WEB SERVICE 上返回大数据量的DATASET
  10. 电子学会青少年编程等级考试Python一级题目解析11