前面写并发的例子时,都是通过 new Thread(...).start() 启动线程。这里把线程的创建、启动和状态单独整理一下。
start 和 run
先看一段代码:
public class ThreadStartDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}, "worker");
thread.run();
thread.start();
thread.join();
}
}输出:
main
worker直接调用 run(),和调用普通方法一样,代码仍然在 main 线程中执行。调用 start() 才会启动新线程,由新线程执行 run() 中的任务。
Thread.currentThread() 取得的是当前正在执行代码的线程。虽然第一次调用写的是 thread.run(),执行它的仍然是主线程,所以打印出来的是 main。
最后的 join() 表示主线程等待 worker 执行结束。它不会启动线程,也不会让两个线程合并成一个。
start() 调用后,新线程什么时候获得 CPU,由线程调度决定。下面两行输出的先后顺序就不能保证:
public class ThreadOrderDemo {
public static void main(String[] args) {
new Thread(() -> System.out.println("worker"), "worker").start();
System.out.println("main");
}
}不能因为先写了 start(),就认为任务一定先于后面的语句执行完。
Thread 和 Runnable
除了把任务传给 Thread,也可以继承 Thread,重写 run():
public class WorkerThread extends Thread {
public WorkerThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
new WorkerThread("worker").start();
}
}前面使用 Lambda 的写法,实际是把一个 Runnable 交给 Thread。写成单独的类就是:
public class WorkerTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Runnable task = new WorkerTask();
new Thread(task, "worker").start();
}
}Runnable 描述要执行的任务,Thread 负责线程本身。一般把任务写成 Runnable 更方便,任务类也不用占掉唯一的类继承位置,后面使用线程池时还能沿用这个任务。
不过,Runnable 并不会自动保证线程安全。同一个任务对象交给多个线程时,如果里面有可变的共享字段,仍然需要处理并发访问。
同一个线程只能启动一次
public class ThreadRestartDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> System.out.println("执行任务"));
thread.start();
thread.join();
thread.start();
}
}第二次调用 start() 会抛出 IllegalThreadStateException。即使第一次执行已经结束,也不能重新启动这个 Thread 对象。
如果还需要执行任务,可以创建新的 Thread。反复提交任务的场景,后面再用线程池处理。
线程状态
Java 用 Thread.State 表示线程状态:
| 状态 | 含义 |
|---|---|
| NEW | 已创建,还没有启动 |
| RUNNABLE | 可以运行,可能正在执行,也可能等待 CPU 等运行资源 |
| BLOCKED | 等待取得 synchronized 使用的监视器锁 |
| WAITING | 没有指定超时时间的等待,例如 wait()、join() |
| TIMED_WAITING | 有时间限制的等待,例如 sleep()、带超时的 join() |
| TERMINATED | 执行结束 |
这些是 Java 层面的状态,不能直接当成操作系统的线程状态。也不是所有“阻塞”都对应 BLOCKED,例如 Thread.sleep() 对应的是 TIMED_WAITING。
用 getState() 可以观察状态:
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> System.out.println("执行任务"));
System.out.println(thread.getState());
thread.start();
thread.join();
System.out.println(thread.getState());
}
}输出:
NEW
执行任务
TERMINATED这里用 join() 等待结束后再读取状态。如果把 getState() 放在 start() 后面直接调用,任务可能正在运行,也可能已经执行完,结果取决于当时的调度。状态只是读取瞬间的情况,不适合用它来控制多个线程之间的执行顺序。
用户线程和守护线程
在 main 中直接创建的线程,默认是用户线程。主线程结束后,只要还有用户线程在运行,JVM 就不会因为主线程结束而退出。
守护线程则不同。当所有用户线程都结束时,JVM 可以退出,不会继续等待守护线程完成任务。
public class DaemonDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("后台任务完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "background");
thread.setDaemon(true);
thread.start();
System.out.println("main 结束");
}
}运行时通常只能看到 main 结束,后台任务还没有执行完,JVM 就退出了。因此不能依赖守护线程完成必须落盘的数据,也不能指望 JVM 退出时一定执行完它的 finally。
setDaemon(true) 要在 start() 前调用。线程的守护属性默认继承创建它的线程,所以“新建线程默认是用户线程”只适用于创建者本身是用户线程的情况。