Skip to content

2026-08-29 · 线程怎么创建、怎么保护共享数据——Spring 并发编程的地基。

Java 多线程

1. 进程与线程

进程:正在运行的程序,有自己独立的内存空间(打开一个记事本就是开一个进程)。

线程:进程内部的执行单元,一个进程至少有一个线程。

进程(程序 + 独立内存)
  ├── 线程1(main 主线程)
  ├── 线程2
  └── 线程3

线程之间共享进程的内存,所以能一起干活、传数据;但也正因为共享,才会产生后面的线程安全问题

为什么要用多线程? 让程序同时干多件事:

  • 服务器同时处理多个用户请求(每个请求一个线程)
  • 读写文件、发邮件这类耗时操作不卡住主流程

所有 Java 程序默认都有一个 main 主线程。main 里写的代码是单线程跑,遇到耗时操作就会卡住——这就是多线程要解决的。

2. 创建线程(日常两种方式)

方式一:实现 Runnable 接口(最常见)

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

// 使用:任务和线程分开
Runnable task = new MyTask();
Thread t = new Thread(task);  // 任务交给线程
t.start();

Lambda 简写(Runnable 是函数式接口):

java
Thread t = new Thread(() -> System.out.println("Lambda 线程"));
t.start();

方式二:实现 Callable 接口(要返回值时)

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

// Callable 和 Runnable 的区别:能返回结果、能抛异常
Callable<Integer> task = () -> 1 + 2;

// 线程不直接接收 Callable,要用 FutureTask 包一层
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();

// 获取结果(会阻塞等待,直到子线程算完)
Integer result = future.get();
System.out.println(result); // 3

⚠️ 启动线程必须用 start(),不是 run()——start() 才是开新线程,直接调 run() 只是普通方法调用,还在主线程里跑。

⚠️ future.get() 会阻塞当前线程直到子线程算完。需要返回值才用 Callable,不需要就用 Runnable。

核心思想Runnable/Callable 定义任务,Thread 负责执行,两者分离。实际项目中很少手动 new Thread,都用线程池(见《Java 线程池》)。

3. 线程生命周期(6 种状态)

状态说明
NEWnew 出来,还没 start()
RUNNABLE已启动,可运行/正在运行
BLOCKED阻塞:等待锁
WAITING无限期等待
TIMED_WAITING限期等待:sleep() 睡固定时间
TERMINATED结束:run() 跑完了
NEW → RUNNABLE → TERMINATED
        ↑   ↓
        BLOCKED / WAITING / TIMED_WAITING

这 6 个状态工作中不用手写判断,但看懂日志里的线程状态(jstack 输出)能帮排查问题。

4. 常用方法速查

方法作用
start()启动线程
sleep(毫秒)当前线程睡一会
join()等某个线程结束
setName() / getName()线程名字(排错有用,日志好认)
currentThread()拿到当前正在跑的线程(静态方法,最常用)

5. 线程安全:多个线程抢同一份数据

看这段经典代码——两个线程同时给同一个数 +1,各加 1 万次,结果应该是 2 万:

java
public class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 这行不是一步,是三步:读值 → 加1 → 写回
    }

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

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

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

        System.out.println(c.count); // 经常不到 20000,比如 17892
    }
}

为什么小于 2 万? 因为 count++ 实际是三步:

线程1:读 count(=99) → 加1(=100) → 写回
线程2:读 count(=99,线程1还没写回) → 加1(=100) → 写回  ← 一次更新丢了

两个线程同时做了"读",都各自 +1 写回,其中一次 +1 被覆盖丢掉了。这叫竞态条件(Race Condition)

⚠️ 不是每次运行都出错,是偶尔出错、值随机。这种"时好时坏"的 bug 最难排查。

解决办法的核心思想:给共享数据加锁,同一时刻只允许一个线程碰它。

6. synchronized:最基础的锁

一个线程进入代码块时自动加锁,其他线程必须等它出来才能进:

java
// 同步方法(等价写法)
public synchronized void increment() {
    count++;
}

// 同步代码块(锁住指定对象,更灵活)
public void increment() {
    synchronized (this) {      // this = 锁当前对象
        count++;
    }
}

锁的是谁?

  • 同步方法/synchronized(this):锁当前对象(new 出来的那个实例)
  • 静态同步方法/synchronized(类.class):锁整个类(所有实例共享)

用 synchronized 修好上面的计数器:

java
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
    // 跑完一定是 20000 ✓
}

使用时注意锁住的对象要一致——两个线程锁的不是同一个对象,等于没锁。静态方法锁类、实例方法锁对象,混用会失效。

7. Lock:更灵活的锁(了解即可)

synchronized 简单但锁死了就只能等。Lock 接口(常用实现 ReentrantLock)提供超时等待、公平排队等能力:

java
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();      // 加锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须释放!用 finally 保证
        }
    }
}
对比synchronizedReentrantLock
写法简单,自动释放要手动 lock() / unlock()(finally 里释放)
超时等待不支持tryLock(3, TimeUnit.SECONDS) 等 3 秒拿不到就放弃
性能现代 JVM 已优化,差别不大差别不大

首选 synchronized(简单安全)。手动 unlock() 忘了写就出问题,只有确实需要"等超时""公平排队"时才换 Lock。

8. 原子类:不用锁的数字操作

对"数字加减"这种场景,JDK 提供了现成的原子类,内部用 CAS 实现,无锁、性能更高

java
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // 原子 +1,线程安全
    }
}
对应常用方法
AtomicIntegerintincrementAndGet() / getAndIncrement() / get()
AtomicLonglong同上
AtomicBooleanbooleanget() / set()

只改数字时优先用原子类(比锁快);逻辑复杂(多步操作)才用 synchronized。

9. 小结:工作中怎么用

  1. 不要手动 new Thread——用线程池(Spring 的 @AsyncThreadPoolTaskExecutor
  2. 共享数据要加锁——首选 synchronized;计数器类场景用原子类
  3. 并发环境用并发集合——ConcurrentHashMap(不要用普通 HashMap)
  4. volatile、wait/notify、死锁分析等——属于底层原理,日常开发基本不写,遇到时再查

到这里,"线程怎么创建、怎么共存、怎么不出错"讲完了。线程池(不手动 new 线程,交给池子管理)是实际项目里每天都要用的东西。