多线程编程核心知识点全面总结
多线程核心:synchronized锁对象而非引用,volatile保证可见性不保证原子性;ReentrantLock支持tryLock和公平锁,Condition精细控制线程;ConcurrentHashMap等并发容器高效;线程池通过ExecutorService管理。
前言
最近在B站观看了马士兵老师关于Java多线程的精彩分享,我将核心知识点进行了系统梳理,整理成这份较为完整的多线程学习笔记。点击下图,即可一览本次讨论的全部内容概览。

正文
多线程基础
1. synchronized 锁定的代码块范围越小,效率自然越高——毕竟线程等待锁的时间会显著缩短。
2. synchronized 锁的是堆内存中的对象实例,而非对象的引用。若用于普通方法,锁的是 this,即当前对象在堆上的地址;若用于静态方法或静态变量,锁的是 Class 对象。一个常见的陷阱是:如果锁定的堆内存被重新赋值指向了新对象,原本使用同一引用的线程将不再受锁的约束。
3. volatile 保证变量在多线程间的可见性,但无法解决原子性问题,因此不能替代 synchronized。若需要原子性操作,可使用 AtomicInteger 等原子类,但其功能不如 synchronized 全面。
4. 切忌使用字符串常量作为锁对象。若两个字符串值相同(例如都是 "hello"),一个线程先锁住变量 a,另一个线程再去锁变量 b,极易引发死锁。这是因为 JVM 常量池会缓存字符串,导致两个引用指向同一对象。
5. wait() 会释放锁,而 notify() 不会——这两个方法是线程间通信的核心机制。使用它们时需要格外谨慎,否则容易导致线程逻辑混乱。
6. CountDownLatch 如同一个门闩:某个线程调用 await() 等待,直到其他线程将计数器减至 0。这与 TypeScript 中的 forkJoin 类似,专门用于等待多个线程完成状态变更后,主线程再继续执行。
7. 写操作加锁而读操作不加锁,大概率会出现脏读。常见的解决方案是 CopyOnWrite 机制——读操作不加锁,写操作时复制一份副本进行修改,保证读到的数据始终一致。
ReentrantLock 重入锁
1. ReentrantLock 可以替代 synchronized,但属于手工锁——必须手动释放,否则线程会一直阻塞。
2. 它和 synchronized 最大的不同在于支持 tryLock:尝试获取锁,获取成功则执行某任务,失败则执行另一任务,而非像 synchronized 那样盲目等待。
3. 通过构造函数的参数 true,可以创建“公平锁”。假设 5 个线程同时等待同一把锁释放,默认是非公平的——谁先抢到算谁的;而公平锁则会按等待时间排序,让等待最久的线程优先获得锁。
4. wait() 通常应与 while 循环配合使用,而非 if。因为 wait() 会释放锁,被唤醒时可能有多个线程同时被唤醒,若仅用 if 判断条件,会出现竞态问题。
5. 需要通知其他线程苏醒时,应使用 notifyAll()。若使用 ReentrantLock,还可借助 Condition 实现精准唤醒:通过 newCondition() 获取 Condition 对象,不同条件可创建不同的 Condition,从而实现更细粒度的线程唤醒控制。
6. ReentrantLock 与传统 Thread 方法的对应关系如下:
| 等待方法 | 通知方法 | 通知全部 |
|---|---|---|
| condition.await() | condition.signal() | condition.signalAll() |
| wait() | notify() | notifyAll() |
7. ThreadLocal 为每个线程提供独立的存储空间——这是典型的以空间换时间的策略。
并发容器
1. 若要确保 List 的 size() 和 remove() 操作具备原子性(例如购票系统),推荐使用 ConcurrentLinkedQueue。它的 poll() 方法先删除元素再返回,若返回 null 表示队列已空——这种“先删后验”的设计十分巧妙。
2. ConcurrentHashMap 取代了 HashTable:后者使用一个大锁锁住整个对象,而前者采用更细粒度的锁机制,每次只锁定一个 segment。更详细的设计可参考这篇文档 https://www.cnblogs.com/heyonggang/p/9112731.html。
3. 在高并发场景下,若 Map 还需要排序功能,可用 ConcurrentSkipListMap 替代 TreeMap。它的插入效率相对较低,但查询速度很快——适合读多写少的场景。
4. CopyOnWriteList 采用写时复制策略:写入数据时复制一份新列表,在新列表上添加元素,最后将引用指向新列表。这种方式下,读操作完全无需加锁,因此读效率高、写效率低。
5. Collections.synchronizedXXX() 可以将未加锁的容器包装成线程安全版本,支持 List、Set、Map 等类型。
6. 高并发环境下常用的两种队列:
- ConcurrentLinkedQueue
- BlockingQueue(LinkedBlockingQueue 是无界队列,ArrayBlockingQueue 是有界队列,DelayQueue 用于定时任务)
队列的三个方法需注意区别:
add():队列满时直接抛出异常offer():队列满时返回false,不会报错put():队列满时阻塞等待,直到有空间释放
7. DelayQueue 中的 take() 方法,若没有消费者取走元素,会一直阻塞等待。
8. SynchronousQueue 容量为 0,不能使用 add(),只能通过 put() 放入一个元素,然后阻塞等待消费者取走。
线程池
1. Executor 是线程池的顶级接口,仅包含一个 execute() 方法,接收 Runnable 参数。
2. ExecutorService 扩展了 Executor,可提交 Runnable 或 Callable 任务,提供了 execute() 和 submit() 两种方法。
3. Callable 与 Runnable 的最大区别在于:call() 方法有返回值,且能够抛出异常。
4. Executors 是一个工具类,提供了五种初始化线程池的方式:
newFixedThreadPool:固定线程数量的线程池newCachedThreadPool:缓存线程池,默认最大线程数为Integer.MAX_VALUE,线程空闲 60 秒后自动销毁newSingleThreadExecutor:单例线程池,保证任务按 FIFO 顺序执行newScheduledThreadPool:支持定时任务的线程池newWorkStealingPool:工作窃取线程池,每个线程拥有独立的任务队列,空闲线程会“窃取”其他线程的任务来执行
5. ThreadPool 的核心思想:保持固定数量的工作线程,空闲时不创建新线程,从而减少资源开销。
6. Future 作为 Callable 的返回值,可用于获取异步计算的结果。
7. 以下是演示代码——分别使用单线程和多线程计算指定范围内的所有质数,对比两者性能差异:
package msb_013;
import ja va.util.ArrayList;
import ja va.util.List;
import ja va.util.concurrent.Callable;
import ja va.util.concurrent.ExecutionException;
import ja va.util.concurrent.ExecutorService;
import ja va.util.concurrent.Executors;
import ja va.util.concurrent.Future;
/**
* 计算指定范围内所有的质数
*/
public class Test03_ParallelComputing {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 单线程计算,耗时约12秒
long start = System.currentTimeMillis();
List list = getPrime(1, 500000);
long end = System.currentTimeMillis();
System.out.println(end - start);
// 使用固定线程池(6个线程)并行计算,耗时缩短到约6秒
ExecutorService service = Executors.newFixedThreadPool(6);
MyTask taskA = new MyTask(1, 200000);
MyTask taskB = new MyTask(200001, 350000);
MyTask taskC = new MyTask(350001, 450000);
MyTask taskD = new MyTask(450001, 500000);
Future> futureA = service.submit(taskA);
Future> futureB = service.submit(taskB);
Future> futureC = service.submit(taskC);
Future> futureD = service.submit(taskD);
start = System.currentTimeMillis();
List listA = futureA.get();
List listB = futureB.get();
List listC = futureC.get();
List listD = futureD.get();
end = System.currentTimeMillis();
System.out.println(end - start);
service.shutdown();
}
static class MyTask implements Callable> {
int start, end;
public MyTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public List call() throws Exception {
return getPrime(start, end);
}
}
public static boolean isPrime(int number) {
for (int i = 2; i < number / 2; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
public static List getPrime(int start, int end) {
List list = new ArrayList<>();
for (int i = start; i <= end; i++) {
if (isPrime(i)) {
list.add(i);
}
}
return list;
}
}
后记
目前仅为初步的知识点整理,后续会持续更新,加入更深入的理解与实践经验,不断完善这份多线程学习笔记。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
Claude Code 必知的14个高效工作流,让你的开发效率提升300%
Claude Code 常用工作流 先分享几项核心判断:Claude Code 真正强大的地方,并非仅仅在于它能编写代码——而是它让“编码”这件事本身变得更加高效且可控。你大概率遇到过这类场景:接手一个陌生项目,花了一整天才能理清架构;线上出现报错,翻遍日志也找不到根本原因;想要重构遗留代码,又担心
阿里云通义AIGC平台完全指南:设计师AI生产力革命
一、写在前面:为什么设计师需要关注AIGC? 如果你还在手动一张一张制作海报、反复修改客户口中“感觉不对”的配色方案、为电商详情页准备几十张不同场景的产品图——那么你一定经历过这些痛点: 创意瓶颈:脑海中有画面,但手绘无法呈现 重复劳动:调整尺寸、更换背景、批量生成变体消耗了大量时间精力 成本焦虑:
零基础毕设代码二次开发:3文件定位法及Vue/Java修改对照表
每年一到毕业季,计算机专业的同学总会陷入一个共同的怪圈:从 GitHub 上扒下来一套代码,或者用 AI 生成一个项目,看起来挺完整的,可导师一句“加个筛选条件”或“换个页面颜色”,瞬间就懵了——不敢改,不会改,怕改崩。是不是很熟悉? 一、为什么AI生成的毕设代码你 "不敢改 "? 1 1 毕业生的三大
反向海淘订单系统:状态机与分布式事务实战设计
先分享一个反直觉的结论:反向海淘订单管理的真正挑战,往往不在于业务逻辑本身,而在于状态流转。一个订单的生命周期拉长到跨国运输,中间涉及的环节多、系统多、参与者多,状态稍有错乱就可能引发连锁事故。Taocarts团队在实践中踩了不少坑,最终沉淀下来的这套状态机与分布式事务方案,成功解决了这一复杂难题。
AI并未抢走程序员饭碗而是更新了编程菜单
AI并未大规模替代程序员,而是改变了职业结构。重复性编码岗位需求下降35%至15%,而AIAgent开发等岗位需求激增187%。开发者焦虑从“被替代”转向“跟不上变化”,60%程序员已使用AI辅助编程。人的核心价值转向架构设计、技术决策和审查AI生成代码,AI技能带来16%薪资溢价。
- 热门数据榜
相关攻略
2026-07-09 16:48
2026-07-09 16:46
2026-07-09 16:46
2026-07-09 16:46
2026-07-09 16:46
2026-07-09 15:45
2026-07-09 15:45
2026-07-09 15:45
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程

