Java并发编程:线程的创建与生命周期

Java 多线程是面试高频考点,也是日常开发绕不开的话题。这篇文章从线程的创建方式讲起,深入到线程状态的流转,配合代码示例把基础打牢。

线程的创建方式

Java 中创建线程主要有两种经典方式,加上 JDK5 引入的 Callable,一共三种常见做法。

方式一:继承 Thread 类

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();  // 启动线程,调用 run()
        t2.start();
    }
}

注意要调用 start() 而不是直接调用 run()。直接调用 run() 只是在当前线程中执行方法体,并不会启动新线程。

方式二:实现 Runnable 接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "线程A");
        Thread t2 = new Thread(new MyRunnable(), "线程B");
        t1.start();
        t2.start();
    }
}

实现 Runnable 接口是更推荐的方式,原因有两个:Java 不支持多继承,继承了 Thread 就不能再继承其他类;Runnable 将任务逻辑和线程对象解耦,同一个 Runnable 实例可以被多个线程共享。

方式三:实现 Callable + FutureTask

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws Exception {
        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        Thread t = new Thread(task);
        t.start();

        // get() 会阻塞当前线程,直到 call() 执行完毕
        Integer result = task.get();
        System.out.println("计算结果: " + result);  // 5050
    }
}

Callable 和 Runnable 的核心区别:Callable 的 call() 方法有返回值,且可以抛出受检异常。

线程的生命周期

Java 线程在整个生命周期中会经历以下六种状态(定义在 Thread.State 枚举中):

NEW(新建) -> 线程对象被创建,但尚未调用 start()

RUNNABLE(可运行) -> 调用 start() 后进入此状态。注意 RUNNABLE 包含了操作系统层面的 Ready 和 Running 两种状态——线程可能在等待 CPU 时间片,也可能正在执行。

BLOCKED(阻塞) -> 线程试图获取一个被其他线程持有的 synchronized 锁时,进入 BLOCKED 状态,直到获取到锁。

WAITING(等待) -> 线程调用了 Object.wait()Thread.join()(不带超时)或 LockSupport.park() 后进入此状态,需要等待其他线程显式唤醒。

TIMED_WAITING(超时等待) -> 与 WAITING 类似,但有超时时间。通过 Thread.sleep(ms)Object.wait(ms)Thread.join(ms) 等方法进入。

TERMINATED(终止) -> run() 方法执行完毕或抛出未捕获异常,线程结束。

状态流转示意:

NEW --start()--> RUNNABLE --获取锁失败--> BLOCKED --获取到锁--> RUNNABLE
                   |                                              |
                   +--wait()/join()---> WAITING ---notify()------>+
                   |                                              |
                   +--sleep(ms)/wait(ms)--> TIMED_WAITING --超时/唤醒-->+
                   |
                   +--run()结束--> TERMINATED

常用的线程控制方法

sleep — 让当前线程休眠

public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始: " + System.currentTimeMillis());
        Thread.sleep(2000);  // 休眠 2 秒
        System.out.println("结束: " + System.currentTimeMillis());
    }
}

sleep() 不会释放锁。如果当前线程持有某个对象的 synchronized 锁,sleep 期间其他线程依然无法进入同步块。

join — 等待另一个线程执行完毕

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("子线程执行完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t.start();
        System.out.println("等待子线程...");
        t.join();  // 主线程阻塞,直到 t 执行完毕
        System.out.println("主线程继续执行");
    }
}

输出顺序是确定的:等待子线程 -> 子线程执行完毕 -> 主线程继续执行。

yield — 让出 CPU 时间片

Thread.yield() 提示调度器当前线程愿意让出 CPU,但调度器可以忽略这个提示。实际开发中很少使用。

synchronized 基础

synchronized 是 Java 中最基本的同步机制,用于保证同一时刻只有一个线程执行某段代码。

public class Counter {
    private int count = 0;

    // 同步方法:锁的是 this 对象
    public synchronized void increment() {
        count++;
    }

    // 同步代码块:可以指定锁对象
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终计数: " + counter.getCount());  // 20000
    }
}

如果去掉 synchronized,最终结果几乎不可能是 20000,因为 count++ 不是原子操作——它实际上包含读取、加一、写回三个步骤,多线程并发时会出现竞态条件。

小结

线程基础看似简单,但很多并发 Bug 都源于对基础概念理解不透。关键记住几点:start()run() 的区别、六种线程状态的流转条件、sleep 不释放锁而 wait 会释放锁、synchronized 的锁对象是谁。后续文章会继续深入 JUC 包中的并发工具。