在Runnable构造方法中向ExecutorService提交this是否存在风险?
你提的这个问题非常关键——在Runnable的构造函数里直接把this提交给ExecutorService是绝对存在风险的,而且很容易引发难以排查的并发问题,下面我给你拆解清楚:
核心风险点:对象未完全初始化就被访问
你的代码里,构造函数先调用Executors.newSingleThreadExecutor().execute(this),然后才执行后续的初始化逻辑。这里的问题在于:ExecutorService的线程可能在构造函数还没执行完、对象还没完全初始化的时候,就调度执行run()方法。
举个具体的场景:假设你的类里有个成员变量private String someData;,构造函数最后一步是给它赋值someData = "initialized";。如果Executor的线程在构造函数走到这一步前就调用了run(),那someData就是默认的null,直接用的话大概率会抛出NullPointerException,或者导致业务逻辑出错。
这种情况不是理论上的极端情况,而是在高并发或者线程调度时机巧合时很容易发生的问题。
额外隐患:内存可见性问题
就算构造函数的初始化代码都执行完了,还有一个容易被忽略的问题:没有同步机制的情况下,Executor的线程可能看不到构造函数里初始化的变量值。
根据Java内存模型(JMM)的规则,对象构造完成后正常发布(比如先把对象赋值给一个变量,再提交给Executor)会有happens-before关系,保证后续线程能看到初始化后的变量。但在构造函数里直接发布this,相当于打破了这个规则,线程池的线程可能看到变量的旧值(比如默认的null或0),这也是一种隐蔽的并发bug。
正确的做法
要避免这些问题,核心原则就是:绝对不要在构造函数里发布this引用,确保对象完全初始化后再提交给ExecutorService。
方案1:外部提交
把对象构造和提交执行的逻辑分开,让外部代码在对象创建完成后再提交:
public class A implements Runnable { public A() { // 先完成所有初始化操作 someOtherInitializations(); } private void someOtherInitializations() { // 你的初始化逻辑 } @Override public void run() { // 任务执行逻辑 } } // 使用时 A task = new A(); Executors.newSingleThreadExecutor().execute(task);
方案2:静态工厂方法
如果希望把提交逻辑封装在类内部,可以用静态工厂方法,确保对象完全初始化后再提交:
public class A implements Runnable { // 私有构造函数,强制通过工厂方法创建 private A() { // 完成所有初始化 someOtherInitializations(); } public static A createAndSubmitTask() { A task = new A(); // 对象已经完全初始化,再提交 Executors.newSingleThreadExecutor().execute(task); return task; } // ... 其他方法 }
总结
在构造过程中发布this(包括提交给Executor、注册到监听器等)是Java并发编程里的常见错误,会导致对象处于不完全初始化的状态被其他线程访问,引发各种难以复现和排查的bug。一定要遵循"先完成初始化,再发布对象"的原则。
内容的提问来源于stack exchange,提问作者Ravi

