Java 线程与多线程:从实战到避坑,一篇彻底打通任督二脉

多线程是 Java 高并发编程的核心基石。用好了,系统如虎添翼;用错了,轻则死锁崩溃,重则内存溢出、生产事故。本文从实战出发,带你从入门到精通,再到避坑,一站式搞定。


一、先把地基打牢:核心概念厘清

概念 一句话定义 类比
进程 操作系统资源分配的最小单位,拥有独立内存空间 一个自给自足的工厂
线程 CPU 调度的最小单位,共享进程资源 工厂里的工人
并发 单 CPU 上多任务交替执行(时间片轮转) 一个厨师快速切换炒三道菜
并行 多 CPU 上多任务同时执行 三个厨师同时炒三道菜

关键认知:Java 线程采用 1:1 映射模型,每个 Java 线程直接对应一个操作系统原生线程。线程创建、切换的开销远小于进程,这正是多线程比多进程更高效的根本原因。


二、四种创建线程的方式:从青铜到王者

🥇 方式一:继承 Thread 类(青铜级)


java

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

// 启动
new MyThread("线程A").start();

⚠️ 致命缺陷:Java 单继承,继承了 Thread 就不能再继承其他类,扩展性极差。不推荐在生产环境使用。


🥈 方式二:实现 Runnable 接口(黄金级·最推荐)


java

class MyRunnable implements Runnable {
    private final String name;
    
    public MyRunnable(String name) {
        this.name = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + " 执行: " + i);
        }
    }
}

// 启动 —— 解耦了线程逻辑与线程对象本身
new Thread(new MyRunnable("线程A"), "窗口1").start();
new Thread(new MyRunnable("线程B"), "窗口2").start();

优势:避免单继承限制,同一个 Runnable 实例可被多个线程共享,是日常开发的主流写法


🥉 方式三:实现 Callable + FutureTask(钻石级)


java

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;
    }
}

// 启动并获取结果
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞直到计算完成
System.out.println("结果: " + result);

核心价值:支持返回值抛出异常,弥补了 Runnable 的短板。

方式 返回值 抛异常 推荐场景
Runnable 火了就忘型任务
Callable 需要结果的计算任务

👑 方式四:线程池(王者级·生产必用)


java

ExecutorService executor = Executors.newFixedThreadPool(3);

for (int i = 0; i < 10; i++) {
    final int taskId = i;
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName() 
            + " 执行任务 " + taskId);
    });
}

executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);

为什么线程池是王道?

优势 说明
降低资源消耗 复用已创建线程,避免反复创建销毁
提高响应速度 任务到达时线程已就绪
防止资源耗尽 限制线程数量,避免无节制创建
统一管理监控 生命周期可控,便于排查问题

🚨 阿里巴巴开发手册明确规定:禁止使用 Executors 创建线程池! 必须用 ThreadPoolExecutor 构造器,因为 Executors.newFixedThreadPool() 使用的是无界队列,任务激增时会导致 OOM


java

// ✅ 推荐写法
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    5,                      // 核心线程数
    10,                     // 最大线程数
    60L, TimeUnit.SECONDS,  // 空闲线程存活时间
    new LinkedBlockingQueue<>(100),  // 有界队列!
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

三、线程的六种状态:一张图看透生死

状态 含义 典型触发
NEW 新建,尚未 start() new Thread()
RUNNABLE 就绪/运行中,等待 CPU 调度 start() 之后
BLOCKED 等待获取监视器锁 synchronized 竞争失败
WAITING 无限期等待 object.wait() / join()
TIMED_WAITING 限时等待 sleep() / wait(timeout)
TERMINATED 执行完毕,已死亡 run() 执行完 / 异常终止

四、线程同步:不加锁,就是在裸奔

🔥 经典问题:100 人抢 10 张票


java

class TicketService {
    private int ticket = 10;
    
    // ❌ 非线程安全 —— 会超卖!
    public void sell() {
        if (ticket > 0) {
            try { Thread.sleep(300); } catch (Exception e) {}
            System.out.println("卖出: " + ticket--);
        }
    }
}

✅ 方案一:synchronized 同步方法


java

public synchronized void sell() {
    if (ticket > 0) {
        try { Thread.sleep(300); } catch (Exception e) {}
        System.out.println("卖出: " + ticket--);
    }
}

✅ 方案二:synchronized 代码块(更精细)


java

public void sell() {
    synchronized (this) {  // 只锁核心逻辑,性能更优
        if (ticket > 0) {
            try { Thread.sleep(300); } catch (Exception e) {}
            System.out.println("卖出: " + ticket--);
        }
    }
}

✅ 方案三:ReentrantLock(高并发推荐)


java

private final ReentrantLock lock = new ReentrantLock();

public void sell() {
    lock.lock();
    try {
        if (ticket > 0) {
            try { Thread.sleep(300); } catch (Exception e) {}
            System.out.println("卖出: " + ticket--);
        }
    } finally {
        lock.unlock(); // ⚠️ 必须在 finally 中释放!
    }
}
方案 适用场景
synchronized 简单场景,代码简洁
ReentrantLock 高并发、需要尝试锁/超时锁
AtomicInteger 简单计数(如 count++

⚠️ volatile 只能保证可见性,不能保证原子性! count++ 是读-改-写三步操作,必须用 AtomicInteger


五、五大深坑:踩一个就够你喝一壶

🕳️ 坑一:线程池参数不当 → OOM

Executors.newFixedThreadPool() 使用无界队列,任务堆积时内存直接爆炸。

解法:用有界队列 + 拒绝策略(CallerRunsPolicy 适合限流降级)。


🕳️ 坑二:ThreadLocal 内存泄漏


java

// ❌ 忘记清理
threadLocal.set(value);
// 线程被回收后,value 永远驻留内存!

// ✅ 必须在 finally 中清除
try {
    threadLocal.set(value);
    // ... 业务逻辑
} finally {
    threadLocal.remove();
}

🕳️ 坑三:死锁 —— 程序假死,无声无息


java

// ❌ 经典死锁:A 锁 X 等 Y,B 锁 Y 等 X
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        synchronized (lockB) { /* ... */ }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lockB) {      // 顺序反了!
        synchronized (lockA) { /* ... */ }
    }
});

解法

  • 固定加锁顺序(所有线程按同一顺序获取锁)
  • 使用 tryLock(timeout) 设置超时
  • jstack 定位死锁线程

🕳️ 坑四:吞掉 InterruptedException


java

// ❌ 最常见的错误
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace(); // 吞掉了!线程中断状态被清除
}

// ✅ 正确做法:恢复中断状态
catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // 恢复中断标志
    throw new RuntimeException(e);
}

🕳️ 坑五:误以为 start() == run()


java

MyThread t = new MyThread();
t.run();  // ❌ 这只是普通方法调用!不会启动新线程
t.start(); // ✅ 这才是真正启动新线程

一个线程对象只能 start() 一次,第二次调用会抛出 IllegalThreadStateException


六、调试神器:jstack 抓线程快照

当线上出现 CPU 飙高、程序假死时:


bash

# 1. 找到 Java 进程 PID
jps -l

# 2. 生成线程快照(重点看 BLOCKED 状态)
jstack -l 12345 > thread_dump.txt

重点关注

  • java.lang.Thread.State: BLOCKED → 锁竞争
  • waiting to lock <0x...> → 正在等哪把锁
  • 死锁信息会明确标出 Found one Java-level deadlock
JVM WAITING 状态显示
HotSpot WAITING (on object monitor)
IBM J9 Waiting for monitor entry

七、最佳实践清单(建议收藏)

✅ 要做 ❌ 别做
用线程池,别手动 new Thread Executors 创建线程池
有界队列 + 拒绝策略 无界队列赌不会满
finally 中释放锁和资源 在 try 里释放锁
volatile 保可见,Atomic 保原子 volatile 替代原子类
join() / CountDownLatch 做线程协作 wait() 不加循环条件判断
CompletableFuture 替代回调地狱 嵌套 Future.get() 阻塞主线程
jstack/VisualVM 排查问题 System.out.println 调多线程

写在最后

多线程编程的本质是在"充分利用资源"和"保证数据安全"之间走钢丝。它不是银弹,而是一把双刃剑——用对了,系统吞吐量翻倍;用错了,死锁、OOM、数据错乱接踵而至。

记住一句话:对多线程保持敬畏之心。 在代码评审时紧盯同步边界,在生产环境做好线程池监控,在测试阶段用并发测试工具系统性验证。只有经过充分验证的代码,才配得上生产环境的信任。

🧠 终极建议:能不用多线程就别用,能用线程池就别手写 Thread,能用 CompletableFuture 就别用 Future.get() 阻塞。简单,才是最高级的并发。

更多推荐