最近在学习 Java 多线程基础,梳理了线程安全产生根源、synchronized 锁用法,以及极易踩坑的死锁问题,把整套知识点整理成通俗易懂的笔记,方便后续复习,也分享给正在学多线程的同学
一、什么是线程安全?
多线程程序中,多个线程同时访问共享资源,如果程序最终运行结果,和单线程顺序执行的结果完全一致,就代表这段代码是线程安全的;反之,结果错乱、数据出错,就是线程不安全。
举个最简单例子:多个线程同时累加同一个计数器,最终数值和预期不符,就是典型的线程安全问题。
二、线程安全问题产生的三大核心原因
- CPU 线程调度具有随机性操作系统随机分配 CPU 时间片,线程执行到一半可能被强行切走暂停,不会完整走完一整套操作。
- 多线程并发读写同一个共享变量多个线程同时修改同一份公共资源,是冲突发生的前提。
- 变量修改操作不具备原子性一行代码底层会拆分成多步 CPU 指令,中途可以被打断。 经典示例:
count++并非一步操作,底层分为三步: - 把变量值读取到寄存器
- 寄存器完成自增计算
- 将计算结果写回内存保存 线程如果在三步中间被切换,就会出现数据覆盖、数值丢失。
三、解决线程安全:synchronized 同步锁
1. 锁的核心作用
加锁后,同一时刻仅有一个线程能持有锁、执行锁内代码,其余线程只能阻塞等待。 语法:
代码执行到右大括号时,会自动释放锁。
原理:通过锁竞争,把并发冲突代码改为串行执行,以此保证数据安全,但串行会牺牲程序运行效率。
2. synchronized 两种使用形式与优缺点
修饰整个方法 直接在方法声明前添加
synchronized,锁范围是整个方法。 缺点:粒度太大,哪怕方法里大部分代码不需要同步,也全部串行执行,严重降低并发性能。同步代码块(推荐)只包裹真正操作共享变量的少量代码,其余逻辑正常并发运行,兼顾安全与性能。
3. 补充关键特性:可重入锁
synchronized是可重入锁:同一个线程拿到一把锁后,可以多次重复获取这把锁,不会自己阻塞自己。 这个特性能规避单锁嵌套场景下的死锁问题。
4. synchronized 自带两大特性(也是死锁必要条件)
- 互斥性锁同一时间只能被一个线程持有;其他线程申请同一锁,直接进入阻塞等待。
- 不可抢占锁一旦被线程占用,其他线程无法强制抢夺,只能等待持有者主动释放。
5. 为什么大部分类不自带同步锁?
加锁会强制部分代码串行,直接削弱程序并发高性能优势。 因此 Java 默认集合、工具类都无内置锁,由开发者根据业务场景手动选择是否加锁。
同时仅靠单锁也有局限:如果一段业务需要多把锁共同保证原子操作,只加单个同步锁依旧会出现线程安全问题。
四、死锁:多锁场景下的严重问题
1. 死锁定义
多个线程互相持有对方需要的锁资源,同时又发起请求获取对方手中的锁,所有线程无限阻塞、僵持等待,程序卡死无法自动恢复。
2. 死锁带来的危害
- 问题复现随机性强:依赖 CPU 调度顺序,测试时很难稳定复现,排查难度极高;
- 线程永久阻塞,持续占用 CPU、内存、连接等系统资源,无法主动释放;
- 业务流程停滞,严重时导致整个服务卡顿、瘫痪。
3. 触发死锁的四个必要条件
- 互斥条件:synchronized 锁天然满足,同一锁只能单线程持有;
- 不可抢占条件:synchronized 锁天然满足,无法强行抢夺他人锁;
- 持有并请求条件:线程已经持有锁 A,不释放,又去申请锁 B(锁嵌套使用); 可重入锁仅解决同一把锁多次获取,无法解决多把不同锁嵌套;
- 循环等待条件:线程之间形成环形等待链:线程 1 等线程 2 的锁,线程 2 等线程 1 的锁。
4. 死锁通用解决办法
破坏循环等待条件即可:给所有锁统一分配编号,规定所有线程必须按锁编号从小到大的顺序申请锁,杜绝环形等待。