Java多线程:从入门到进阶

Java多线程:从入门到进阶

1. 引入:为什么需要多线程?

1.1 单线程的瓶颈

假设你要下载三个文件,单线程的做法是:一个个下载,总时间 = 文件1 + 文件2 + 文件3。

java 复制代码
downloadFile1();  // 等待完成
downloadFile2();  // 再等
downloadFile3();  // 最后
// 总耗时很长

如果能让三个下载同时进行,总时间 ≈ 最慢的那个文件 ------ 这就是多线程的价值。

1.2 多线程能做什么

  • 提升响应速度:UI不会卡死,后台任务异步处理
  • 充分利用CPU:多核CPU真正并行
  • 提高资源利用率:一个线程等待IO时,其他线程可继续工作

1.3 需要注意

多线程不是完美的,它带来:

  • 线程安全问题:多个线程同时修改共享数据
  • 性能开销:创建、切换线程有成本
  • 死锁风险:互相等待对方释放资源

2. 线程的创建方式

Java中有三种常见方式创建线程。

2.1 继承Thread类

java 复制代码
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行");
    }
}

// 使用
MyThread t1 = new MyThread();
t1.start();  // 启动线程(不是调用run)

特点:

  • 简单直接
  • 单继承局限(不能再继承其他类)

2.2 实现Runnable接口

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行");
    }
}

// 使用
Thread t1 = new Thread(new MyRunnable());
t1.start();

Lambda简化(推荐):

java 复制代码
Thread t1 = new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + "执行");
});
t1.start();

特点:

  • 避免单继承局限
  • 更适合资源共享(多个线程执行同一个Runnable实例)

2.3 实现Callable接口 + FutureTask

有返回值的线程:

java 复制代码
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return 100;
    }
}

// 使用
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(task);
t1.start();
Integer result = task.get();  // 获取返回值(会阻塞直到完成)

特点:

  • 可以有返回值
  • 可以抛异常

2.4 三种方式对比

方式 是否有返回值 可否抛异常 是否单继承 适用场景
继承Thread 否 否 是 简单任务
Runnable 否 否 否 推荐,资源共享
Callable 是 是 否 需要返回结果

3. 线程的常用方法

方法 作用
start() 启动线程,JVM调用run()
run() 线程执行体,不要直接调用
sleep(long millis) 线程睡眠(让出CPU,不释放锁)
join() 等待该线程终止
yield() 礼让,让出CPU,重新参与竞争
setPriority(int) 设置优先级(1~10,默认5)
interrupt() 中断线程(设置中断标志)
currentThread() 获取当前执行的线程
java 复制代码
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("t1: " + i);
        Thread.sleep(500);
    }
});
Thread t2 = new Thread(() -> {
    try {
        t1.join();  // t2等待t1执行完
    } catch (InterruptedException e) {}
    System.out.println("t2执行");
});
t1.start();
t2.start();

4. 线程的生命周期(状态)

线程有6种状态:

复制代码
NEW → RUNNABLE → TERMINATED
         ↙     ↘
      BLOCKED   WAITING / TIMED_WAITING
状态 说明
NEW 创建但未启动(还没调用start)
RUNNABLE 可运行状态(正在JVM中执行,可能等待CPU)
BLOCKED 阻塞,等待获取锁(synchronized)
WAITING 无限等待(wait()、join()无超时)
TIMED_WAITING 限时等待(sleep()、wait(timeout))
TERMINATED 结束(run执行完)
java 复制代码
Thread t = new Thread(() -> {});
System.out.println(t.getState());  // NEW
t.start();
System.out.println(t.getState());  // RUNNABLE(可能瞬间变化)

5. 线程安全问题

5.1 问题演示

多个线程同时修改共享变量:

java 复制代码
class Counter {
    private int count = 0;
    public void increment() { count++; }
    public int getCount() { return count; }
}

// 两个线程各加1000次
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(counter.getCount());  // 期望2000,实际可能小于2000

原因 :count++不是原子操作(读-改-写),线程交叉执行导致数据丢失。

5.2 解决方案:synchronized

同步代码块
java 复制代码
public void increment() {
    synchronized (this) {  // 锁当前对象
        count++;
    }
}
同步方法
java 复制代码
public synchronized void increment() {  // 等效于synchronized(this)
    count++;
}
静态同步方法(类锁)
java 复制代码
public static synchronized void staticMethod() { // 锁Class对象
    // ...
}

5.3 锁的选择

方式 锁对象 影响范围
非静态同步方法 this 同一个对象的多个方法互斥
静态同步方法 类名.class 所有对象的该方法互斥
同步代码块 自定义 更灵活,可减小锁粒度

6. 线程通信:wait/notify

生产者-消费者模式经典示例:

java 复制代码
class Product {
    private int count = 0;
    private final int MAX = 10;
    
    public synchronized void produce() throws InterruptedException {
        while (count >= MAX) {
            wait();  // 满了,等待消费
        }
        count++;
        System.out.println("生产,库存:" + count);
        notifyAll(); // 唤醒等待的消费者
    }
    
    public synchronized void consume() throws InterruptedException {
        while (count <= 0) {
            wait();  // 空了,等待生产
        }
        count--;
        System.out.println("消费,库存:" + count);
        notifyAll();
    }
}

6.1 wait/notify的规则

规则 说明
必须在synchronized块内 否则抛IllegalMonitorStateException
wait释放锁 让出锁,进入WAITING
notify不释放锁 唤醒另一个线程,但需要当前线程退出同步块后才实际竞争
用while循环判断条件 防止虚假唤醒

6.2 wait和sleep的区别

维度 wait sleep
所属 Object的方法 Thread的静态方法
释放锁 是 否
需要synchronized 是 否
唤醒条件 notify/notifyAll 时间到或interrupt

7. 线程安全的集合

集合 线程安全方式 适用场景
Vector / Hashtable 内部方法synchronized 遗留类,不推荐
Collections.synchronizedXxx 包装成同步 简单的同步需求
CopyOnWriteArrayList 写时复制 读多写少
ConcurrentHashMap 分段锁/CAS 高并发Map
BlockingQueue 阻塞队列 生产者-消费者
java 复制代码
// 推荐:ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();

// 线程安全的List
List<String> list = new CopyOnWriteArrayList<>();

8. 并发问题的底层根源(了解)

根源 说明
可见性 一个线程修改共享变量,其他线程看不到(用volatile解决)
原子性 操作不是一步完成(用synchronized或Lock)
有序性 指令重排序(volatile禁止重排序)

volatile关键字

  • 保证可见性:修改立刻写回主存
  • 禁止指令重排序
  • 不保证原子性 (如count++仍不安全)
java 复制代码
volatile boolean flag = true;  // 适合用作开关

9. 线程池

9.1 为什么需要线程池?

每次创建和销毁线程都有开销:

  • new Thread() 创建线程 → 内存分配、系统调用
  • 线程用完销毁 → 垃圾回收

如果有大量短任务,反复创建销毁线程会严重影响性能。

线程池的优势:

  • 复用线程,减少创建销毁开销
  • 控制线程数量,防止资源耗尽
  • 统一管理任务队列

9.2 线程池的核心:ThreadPoolExecutor

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 空闲线程存活时间
    TimeUnit unit,         // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

执行流程:

复制代码
提交任务 → 核心线程未满 → 创建核心线程执行
         → 核心线程已满 → 任务放入队列
         → 队列已满 → 创建非核心线程执行
         → 达到最大线程 → 执行拒绝策略

9.3 五种常见的线程池

方法 说明 特点
newFixedThreadPool(n) 固定线程数 核心=最大=n,无超时
newCachedThreadPool() 缓存线程池 核心=0,最大=无限,空闲60秒回收
newSingleThreadExecutor() 单线程池 核心=最大=1,保证顺序执行
newScheduledThreadPool(n) 定时任务池 支持延迟和执行周期任务
newWorkStealingPool() 工作窃取池 基于ForkJoinPool(Java 8+)
java 复制代码
// 固定大小线程池
ExecutorService pool = Executors.newFixedThreadPool(5);

// 提交任务
pool.execute(() -> {
    System.out.println("无返回值任务");
});

Future<String> future = pool.submit(() -> {
    return "有返回值任务";
});

// 关闭线程池
pool.shutdown();     // 不再接受新任务,等待已有任务完成
pool.shutdownNow();  // 尝试停止所有执行中任务

9.4 任务队列的选择

队列类型 特点 适用场景
ArrayBlockingQueue 有界,数组结构 任务数量可控
LinkedBlockingQueue 有界/无界,链表结构 默认无界,小心OOM
SynchronousQueue 不存储任务,需立即处理 cached线程池使用
PriorityBlockingQueue 有优先级 任务需排序

9.5 使用线程池的注意事项

易错点 说明
不推荐Executors.newFixedThreadPool 阻塞队列无界,可能OOM
不推荐Executors.newCachedThreadPool 最大线程无限,可能创建过多线程
忘记关闭线程池 程序可能不会退出
shutdown和shutdownNow混淆 shutdown不再收任务但执行已有;shutdownNow尝试中断
异常被吞掉 submit返回的Future可以捕获异常

阿里巴巴规范 :不要用Executors创建线程池,要自定义ThreadPoolExecutor。

9.6 完整示例

java 复制代码
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2, 5, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    r -> {
        Thread t = new Thread(r);
        t.setName("MyPool-" + System.currentTimeMillis());
        return t;
    },
    (r, executor) -> {
        System.out.println("任务被拒绝:" + r.toString());
    }
);

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

pool.shutdown();
try {
    if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
        pool.shutdownNow();
    }
} catch (InterruptedException e) {
    pool.shutdownNow();
}

10. 死锁

10.1 死锁示例

两个线程互相持有对方需要的锁:

java 复制代码
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        Thread.sleep(100);
        synchronized (lockB) { }
    }
});
Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        Thread.sleep(100);
        synchronized (lockA) { }
    }
});

10.2 避免方法

  • 按固定顺序获取锁
  • 使用tryLock超时放弃
  • 减少锁的嵌套

11. 易错点总结

易错点 错误原因 正确做法
直接调用run() 以为会启动线程 调用start()
静态方法synchronized混用 锁对象不同 明确锁是类还是实例
wait/notify不用while判断 虚假唤醒 用while循环
线程安全问题只想到synchronized 简单粗暴 考虑volatile、原子类、安全集合
死锁排查困难 互相等待 用jstack分析
共享变量不加同步 以为不会并发 所有共享变量都要同步
Executors创建线程池 无界队列OOM 自定义ThreadPoolExecutor
忘记关闭线程池 程序不退出 finally中shutdown

12. 总结对比表

概念 核心要点
创建线程 三种方式:Thread、Runnable、Callable
生命周期 NEW → RUNNABLE → TERMINATED(中间有阻塞/等待)
线程安全 synchronized、Lock、原子类、安全集合
线程通信 wait/notify、BlockingQueue
并发关键字 volatile(可见性、禁止重排)、synchronized(原子性)
线程池 ThreadPoolExecutor核心参数、任务队列
死锁 循环等待,按顺序加锁或tryLock
相关推荐
Java后端的Ai之路21 分钟前
Python进阶探索24_应用案例_linux系统的监控
linux·开发语言·python·应用·探索
程序员清风35 分钟前
PydanticAI 实战:用类型安全构建可靠的 Python Agent
开发语言·python·安全
落魄实习生38 分钟前
Agent Scope Java 2.x 系列【10】Middleware
java·开发语言·ai
Ramble_Naylor1 小时前
async/await:让一个线程同时等很多件事
开发语言·rust
ZealSinger1 小时前
Spring7 RestTemplate弃用迁移
java·spring·后端开发·restclient
迅猛龙办公室2 小时前
Python实现绘制同切圆
开发语言·python
朝朝辞暮i2 小时前
C++ 第 13 课:值传递 —— 为什么函数里改了,外面却没变?
java·c++·算法
弹简特2 小时前
【Java项目-企悦抽】16-抽奖模块01-获取活动完整信息接口实现
java·开发语言·状态模式·springboot
言乐62 小时前
Python语音检索
开发语言·python·django·virtualenv·pygame
BD_Marathon2 小时前
多轮对话聊天机器人
开发语言·机器人·c#