线程基础:线程的创建与启动

聊到并发编程,第一个绕不开的话题就是——线程怎么创建?

我记得刚入行那会儿,面试官最爱问的就是「创建线程有几种方式」。那时候我背得滚瓜烂熟:三种,继承Thread、实现Runnable、实现Callable。但说实话,真正用起来,里面的门道可不止这么简单。

1. 继承Thread类

这是最直观的方式。你写一个类,继承Thread,重写run方法,然后调用start()启动。代码长这样:

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行中:" + Thread.currentThread().getName());
    }
}

// 启动
MyThread t = new MyThread();
t.start();

嗯,这里要注意:千万别直接调run()。调run()只是普通方法调用,不会启动新线程。start()才是告诉JVM「嘿,给我开个新线程」。我在项目中遇到过有人调了run(),结果发现代码跑在主线程上,排查了半天才找到原因。

避坑指南: 我曾经接手过一个老项目,里面大量使用继承Thread的方式。后来想扩展功能,发现类已经被Thread「占住」了,没法再继承别的类。这就是继承方式的硬伤——Java单继承,你用了就没得选了。

2. 实现Runnable接口

我个人更推荐这种方式。说白了,就是把任务和线程分开。任务就是Runnable,线程是载体。

public class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("任务执行中:" + Thread.currentThread().getName());
    }
}

// 启动
Thread t = new Thread(new MyTask());
t.start();

你想想看,这样设计的好处是什么?你的任务类还可以继承别的类,灵活性高很多。而且多个线程可以共享同一个Runnable实例,方便做数据共享。

我在项目中做过一个爬虫系统,就是用Runnable的方式。每个URL封装成一个任务,丢给线程池去跑。如果当时用继承Thread,代码会臃肿很多。

3. 实现Callable接口

Runnable有个问题——它没有返回值,也不能抛异常。那如果我想让线程执行完返回一个结果呢?

这时候Callable就派上用场了。它配合FutureTask或者线程池使用,可以拿到线程的执行结果。

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "任务执行结果";
    }
}

// 配合FutureTask使用
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread t = new Thread(futureTask);
t.start();
String result = futureTask.get(); // 获取结果,会阻塞
我的习惯: 实际开发中,我几乎不用继承Thread。Runnable和Callable才是主流。尤其是配合线程池使用时,Callable能帮你优雅地处理异步结果。

线程的生命周期

线程不是一启动就完事了。它有自己的「一生」。Java线程有6种状态,用Thread.State枚举表示:

状态 说明
NEW 新建,还没调用start()
RUNNABLE 可运行,等待CPU调度
BLOCKED 阻塞,等待锁
WAITING 等待,需要被显式唤醒
TIMED_WAITING 超时等待,到时间自动唤醒
TERMINATED 终止,执行完毕

为什么会这样设计?其实很好理解。线程在执行过程中,可能会因为各种原因「暂停」——比如等锁、等IO、等别的线程通知。这些暂停状态被细分出来,方便我们排查问题。

我记得有一次线上服务CPU飙高,用jstack dump线程栈一看,大量线程卡在BLOCKED状态。顺着锁的链条查下去,发现是某个资源没释放导致的死锁。嗯,如果不懂生命周期,你连问题都看不懂。

关键点: RUNNABLE状态其实包含了「正在运行」和「等待CPU时间片」两种情况。在操作系统层面,这两者是分开的,但JVM把它们合并了。你想想看,这对我们排查问题有什么影响?——你没法区分线程是「真的在干活」还是「在排队等CPU」。

线程的优先级

Java线程有10个优先级,从1到10。默认是5。你可以用setPriority()来设置。

Thread t = new Thread(() -> System.out.println("低优先级"));
t.setPriority(Thread.MIN_PRIORITY); // 1
t.start();

但说实话,优先级这个东西,别太当真。它只是给操作系统的一个「建议」,最终调度权在操作系统手里。我在Linux上测试过,高优先级线程确实会多分到一些CPU时间,但Windows上的表现就不太一样。

避坑指南: 我曾经试图用优先级来解决一个性能问题,结果发现根本不可靠。不同操作系统、不同JVM实现,优先级的行为都不一样。别把业务逻辑依赖在线程优先级上,那是给自己挖坑。

守护线程

Java线程分两种:用户线程和守护线程。守护线程是「后台服务」性质的,比如垃圾回收线程就是守护线程。

关键区别:当所有用户线程都结束时,JVM会直接退出,不管守护线程是否还在运行。

Thread daemonThread = new Thread(() -> {
    while (true) {
        System.out.println("守护线程运行中...");
    }
});
daemonThread.setDaemon(true);
daemonThread.start();

嗯,这里要注意:setDaemon(true)必须在start()之前调用。否则会抛IllegalThreadStateException。

我在项目中用守护线程做过心跳检测。主线程处理业务,守护线程定期发心跳。主线程一退出,守护线程自动结束,不用手动管理。省心。

我的建议: 守护线程适合做监控、日志、定时清理这类辅助工作。但别把重要业务逻辑放进去——JVM退出时不保证守护线程能执行完。

好了,线程的创建与启动、生命周期、优先级、守护线程,这些基础概念就聊到这儿。下一章我们深入线程安全的核心——synchronized和锁机制。到时候我会分享一些实际项目中踩过的坑,保证让你有收获。