Java多线程:从入门到进阶
Java多线程:从入门到进阶
1. 引入:为什么需要多线程?
1.1 单线程的瓶颈
假设你要下载三个文件,单线程的做法是:一个个下载,总时间 = 文件1 + 文件2 + 文件3。
downloadFile1();// 等待完成downloadFile2();// 再等downloadFile3();// 最后// 总耗时很长如果能让三个下载同时进行,总时间 ≈ 最慢的那个文件 —— 这就是多线程的价值。
1.2 多线程能做什么
- 提升响应速度:UI不会卡死,后台任务异步处理
- 充分利用CPU:多核CPU真正并行
- 提高资源利用率:一个线程等待IO时,其他线程可继续工作
1.3 需要注意
多线程不是完美的,它带来:
- 线程安全问题:多个线程同时修改共享数据
- 性能开销:创建、切换线程有成本
- 死锁风险:互相等待对方释放资源
2. 线程的创建方式
Java中有三种常见方式创建线程。
2.1 继承Thread类
classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"执行");}}// 使用MyThreadt1=newMyThread();t1.start();// 启动线程(不是调用run)特点:
- 简单直接
- 单继承局限(不能再继承其他类)
2.2 实现Runnable接口
classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"执行");}}// 使用Threadt1=newThread(newMyRunnable());t1.start();Lambda简化(推荐):
Threadt1=newThread(()->{System.out.println(Thread.currentThread().getName()+"执行");});t1.start();特点:
- 避免单继承局限
- 更适合资源共享(多个线程执行同一个Runnable实例)
2.3 实现Callable接口 + FutureTask
有返回值的线程:
classMyCallableimplementsCallable<Integer>{@OverridepublicIntegercall()throwsException{Thread.sleep(1000);return100;}}// 使用FutureTask<Integer>task=newFutureTask<>(newMyCallable());Threadt1=newThread(task);t1.start();Integerresult=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() | 获取当前执行的线程 |
Threadt1=newThread(()->{for(inti=0;i<5;i++){System.out.println("t1: "+i);Thread.sleep(500);}});Threadt2=newThread(()->{try{t1.join();// t2等待t1执行完}catch(InterruptedExceptione){}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执行完) |
Threadt=newThread(()->{});System.out.println(t.getState());// NEWt.start();System.out.println(t.getState());// RUNNABLE(可能瞬间变化)5. 线程安全问题
5.1 问题演示
多个线程同时修改共享变量:
classCounter{privateintcount=0;publicvoidincrement(){count++;}publicintgetCount(){returncount;}}// 两个线程各加1000次Countercounter=newCounter();Threadt1=newThread(()->{for(inti=0;i<1000;i++)counter.increment();});Threadt2=newThread(()->{for(inti=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
同步代码块
publicvoidincrement(){synchronized(this){// 锁当前对象count++;}}同步方法
publicsynchronizedvoidincrement(){// 等效于synchronized(this)count++;}静态同步方法(类锁)
publicstaticsynchronizedvoidstaticMethod(){// 锁Class对象// ...}5.3 锁的选择
| 方式 | 锁对象 | 影响范围 |
|---|---|---|
| 非静态同步方法 | this | 同一个对象的多个方法互斥 |
| 静态同步方法 | 类名.class | 所有对象的该方法互斥 |
| 同步代码块 | 自定义 | 更灵活,可减小锁粒度 |
6. 线程通信:wait/notify
生产者-消费者模式经典示例:
classProduct{privateintcount=0;privatefinalintMAX=10;publicsynchronizedvoidproduce()throwsInterruptedException{while(count>=MAX){wait();// 满了,等待消费}count++;System.out.println("生产,库存:"+count);notifyAll();// 唤醒等待的消费者}publicsynchronizedvoidconsume()throwsInterruptedException{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 | 阻塞队列 | 生产者-消费者 |
// 推荐:ConcurrentHashMapMap<String,String>map=newConcurrentHashMap<>();// 线程安全的ListList<String>list=newCopyOnWriteArrayList<>();8. 并发问题的底层根源(了解)
| 根源 | 说明 |
|---|---|
| 可见性 | 一个线程修改共享变量,其他线程看不到(用volatile解决) |
| 原子性 | 操作不是一步完成(用synchronized或Lock) |
| 有序性 | 指令重排序(volatile禁止重排序) |
volatile关键字
- 保证可见性:修改立刻写回主存
- 禁止指令重排序
- 不保证原子性(如
count++仍不安全)
volatilebooleanflag=true;// 适合用作开关9. 线程池
9.1 为什么需要线程池?
每次创建和销毁线程都有开销:
new Thread()创建线程 → 内存分配、系统调用- 线程用完销毁 → 垃圾回收
如果有大量短任务,反复创建销毁线程会严重影响性能。
线程池的优势:
- 复用线程,减少创建销毁开销
- 控制线程数量,防止资源耗尽
- 统一管理任务队列
9.2 线程池的核心:ThreadPoolExecutor
publicThreadPoolExecutor(intcorePoolSize,// 核心线程数intmaximumPoolSize,// 最大线程数longkeepAliveTime,// 空闲线程存活时间TimeUnitunit,// 时间单位BlockingQueue<Runnable>workQueue,// 任务队列ThreadFactorythreadFactory,// 线程工厂RejectedExecutionHandlerhandler// 拒绝策略)执行流程:
提交任务 → 核心线程未满 → 创建核心线程执行 → 核心线程已满 → 任务放入队列 → 队列已满 → 创建非核心线程执行 → 达到最大线程 → 执行拒绝策略9.3 五种常见的线程池
| 方法 | 说明 | 特点 |
|---|---|---|
newFixedThreadPool(n) | 固定线程数 | 核心=最大=n,无超时 |
newCachedThreadPool() | 缓存线程池 | 核心=0,最大=无限,空闲60秒回收 |
newSingleThreadExecutor() | 单线程池 | 核心=最大=1,保证顺序执行 |
newScheduledThreadPool(n) | 定时任务池 | 支持延迟和执行周期任务 |
newWorkStealingPool() | 工作窃取池 | 基于ForkJoinPool(Java 8+) |
// 固定大小线程池ExecutorServicepool=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 完整示例
ThreadPoolExecutorpool=newThreadPoolExecutor(2,5,60L,TimeUnit.SECONDS,newArrayBlockingQueue<>(100),r->{Threadt=newThread(r);t.setName("MyPool-"+System.currentTimeMillis());returnt;},(r,executor)->{System.out.println("任务被拒绝:"+r.toString());});// 提交任务for(inti=0;i<200;i++){finalinttaskId=i;pool.execute(()->{System.out.println(Thread.currentThread().getName()+"执行任务"+taskId);});}pool.shutdown();try{if(!pool.awaitTermination(60,TimeUnit.SECONDS)){pool.shutdownNow();}}catch(InterruptedExceptione){pool.shutdownNow();}10. 死锁
10.1 死锁示例
两个线程互相持有对方需要的锁:
ObjectlockA=newObject();ObjectlockB=newObject();Threadt1=newThread(()->{synchronized(lockA){Thread.sleep(100);synchronized(lockB){}}});Threadt2=newThread(()->{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 |
