1. 概述

本文将探讨一个 Java 并发编程中的经典陷阱:不要在构造函数中启动线程

我们会先简要介绍 Java 中的“发布(publication)”与“逃逸(escape)”概念,然后结合线程启动的场景,说明为什么这种写法容易踩坑。虽然看起来只是一个小细节,但在高并发环境下,它可能导致对象处于未初始化完成的状态就被其他线程访问,从而引发难以排查的 bug。

2. 对象发布与逃逸

对象发布(Publication):当你把一个对象暴露给外部作用域时,就相当于“发布了”这个对象。比如:

  • 将对象返回给调用方
  • 赋值给一个 public 静态变量
  • 作为参数传递给其他类的方法

对象逃逸(Escape):如果一个对象在尚未构造完成时就被发布了,那就发生了“逃逸”。这是非常危险的操作,尤其在多线程环境下。

⚠️ 最典型的逃逸场景之一是:**this 引用在构造函数执行期间就泄露了出去**。

一旦 this 被其他线程拿到,而此时对象的字段可能还没初始化完毕,其他线程看到的就是一个“半成品”对象 —— 这会直接破坏线程安全,导致不可预测的行为。

3. 线程启动导致 this 逃逸

🔥 最常见的 this 逃逸方式之一,就是在构造函数里直接启动线程

来看一个典型反例:

public class LoggerRunnable implements Runnable {

    public LoggerRunnable() {
        Thread thread = new Thread(this); // this 在这里逃逸了
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("Started...");
    }
}

上面这段代码的问题在于:

  • this(即当前正在构造的对象)被传入了 Thread 构造器
  • 线程立即 start(),意味着另一个线程可能马上开始执行 run()
  • 此时构造函数还没执行完,对象状态不完整

📌 JVM 并不能保证构造函数的所有指令都在 thread.start() 之前完成。由于指令重排序和内存可见性问题,新线程可能看到一个未初始化完全的对象。

3.1 隐式逃逸也不安全

即使你没有显式传 this,只要用了匿名内部类,依然可能逃逸:

public class ImplicitEscape {
    
    public ImplicitEscape() {
        Thread t = new Thread() {

            @Override
            public void run() {
                System.out.println("Started...");
            }
        };
        
        t.start();
    }
}

⚠️ 虽然看起来没传 this,但匿名内部类会隐式持有外部类的引用。也就是说,this 依然通过闭包的方式逃逸了。

🎯 结论:
创建线程本身没问题,但在构造函数中直接调用 start() 是高风险操作,无论是显式还是隐式传递 this,都可能导致对象状态不一致。

3.2 安全替代方案

✅ 正确做法是:延迟线程的启动,确保对象构造完成后再暴露给其他线程。

推荐做法:提供一个独立的 start() 方法。

public class SafePublication implements Runnable {
    
    private final Thread thread;
    
    public SafePublication() {
        thread = new Thread(this);
        // ✅ 仅创建线程,不启动
    }

    @Override
    public void run() {
        System.out.println("Started...");
    }
    
    public void start() {
        thread.start(); // ✅ 构造完成后再启动
    }
}

使用方式:

SafePublication publication = new SafePublication();
publication.start(); // 对象已完全构造,安全启动

📌 这样做虽然 this 仍然被传给了 Thread,但关键区别在于:

  • thread.start() 是在构造函数返回之后才调用的
  • 此时对象已“安全发布”,不会出现部分初始化的问题

4. 总结

  • ❌ 禁止在构造函数中直接 start() 线程,否则 this 可能逃逸
  • ⚠️ 匿名内部类也会导致隐式 this 逃逸
  • ✅ 正确姿势:构造函数只做初始化,线程启动交给外部显式调用(如 start() 方法)
  • 📚 更深入的内容可参考经典书籍《Java Concurrency in Practice

所有示例代码均已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-advanced-3


原始标题:Why Not to Start a Thread in the Constructor?