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解决)
原子性 操作不是一步完成(用synchronizedLock
有序性 指令重排序(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
相关推荐
z200509301 小时前
C++中的右值引用
开发语言·c++
SilentSamsara1 小时前
迭代器协议:`__iter__` / `__next__` 的完整执行流程
开发语言·人工智能·python·算法·机器学习
平凡但不平庸的码农1 小时前
Go Channel详解
开发语言·后端·golang
NE_STOP1 小时前
Redis--Redis分布式系统的原理与实操
java
laomocoder1 小时前
Project-Nexus-WAN-跨公网Agent对话
开发语言·php
子安柠1 小时前
深入理解 Go 语言文件操作:从基础到最佳实践
开发语言·后端·golang
代码中介商1 小时前
C++文件流操作全解析
开发语言·c++
Forget_85501 小时前
RHEL——Kubernetes容器编排平台(二)
java·开发语言
Achou.Wang1 小时前
go语言中使用等待组(waitgroups)和内存屏障(barriers)进行同步
开发语言·后端·golang