前面写并发的例子时,都是通过 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() 前调用。线程的守护属性默认继承创建它的线程,所以“新建线程默认是用户线程”只适用于创建者本身是用户线程的情况。

最后修改:2018 年 01 月 03 日
如果觉得我的文章对你有用,请随意赞赏