线程基础:线程的创建与启动
聊到并发编程,第一个绕不开的话题就是——线程怎么创建?
我记得刚入行那会儿,面试官最爱问的就是「创建线程有几种方式」。那时候我背得滚瓜烂熟:三种,继承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(),结果发现代码跑在主线程上,排查了半天才找到原因。
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(); // 获取结果,会阻塞
线程的生命周期
线程不是一启动就完事了。它有自己的「一生」。Java线程有6种状态,用Thread.State枚举表示:
| 状态 | 说明 |
|---|---|
| NEW | 新建,还没调用start() |
| RUNNABLE | 可运行,等待CPU调度 |
| BLOCKED | 阻塞,等待锁 |
| WAITING | 等待,需要被显式唤醒 |
| TIMED_WAITING | 超时等待,到时间自动唤醒 |
| TERMINATED | 终止,执行完毕 |
为什么会这样设计?其实很好理解。线程在执行过程中,可能会因为各种原因「暂停」——比如等锁、等IO、等别的线程通知。这些暂停状态被细分出来,方便我们排查问题。
我记得有一次线上服务CPU飙高,用jstack dump线程栈一看,大量线程卡在BLOCKED状态。顺着锁的链条查下去,发现是某个资源没释放导致的死锁。嗯,如果不懂生命周期,你连问题都看不懂。
线程的优先级
Java线程有10个优先级,从1到10。默认是5。你可以用setPriority()来设置。
Thread t = new Thread(() -> System.out.println("低优先级"));
t.setPriority(Thread.MIN_PRIORITY); // 1
t.start();
但说实话,优先级这个东西,别太当真。它只是给操作系统的一个「建议」,最终调度权在操作系统手里。我在Linux上测试过,高优先级线程确实会多分到一些CPU时间,但Windows上的表现就不太一样。
守护线程
Java线程分两种:用户线程和守护线程。守护线程是「后台服务」性质的,比如垃圾回收线程就是守护线程。
关键区别:当所有用户线程都结束时,JVM会直接退出,不管守护线程是否还在运行。
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中...");
}
});
daemonThread.setDaemon(true);
daemonThread.start();
嗯,这里要注意:setDaemon(true)必须在start()之前调用。否则会抛IllegalThreadStateException。
我在项目中用守护线程做过心跳检测。主线程处理业务,守护线程定期发心跳。主线程一退出,守护线程自动结束,不用手动管理。省心。
好了,线程的创建与启动、生命周期、优先级、守护线程,这些基础概念就聊到这儿。下一章我们深入线程安全的核心——synchronized和锁机制。到时候我会分享一些实际项目中踩过的坑,保证让你有收获。