线程安全问题与同步机制
线程安全的本质问题
线程安全问题源于多线程环境下对共享资源(数据或状态)的非原子性、非可见性、非有序性访问,导致程序行为不符合预期。
主要表现如下:
-
竞态条件(Race Condition):多个线程对同一资源进行非原子操作,导致结果依赖线程执行顺序。
-
示例:两个线程同时执行
count++
(非原子操作,实际包含读-改-写三步)。
-
-
内存可见性问题:线程修改共享变量后,其他线程无法立即看到最新值(CPU缓存与主存不一致)。
-
示例:线程A修改
flag=true
,线程B可能仍读取到旧值flag=false
。
-
-
指令重排序:编译器或处理器优化可能导致代码执行顺序与源码不一致,破坏预期逻辑。
-
示例:单例模式的双重检查锁定中,未使用
volatile
可能导致对象未初始化完成就被使用。
-
同步机制的核心目标
-
原子性:确保操作不可分割,要么全部执行,要么不执行。
-
可见性:一个线程修改共享变量后,其他线程能立即感知变化。
-
有序性:禁止编译器和处理器对代码顺序进行破坏逻辑的优化。
线程的互斥同步方式有哪些? 如何比较和选择?
互斥同步的主要方式
机制 | 实现原理 | 特点 | 适用场景 |
---|---|---|---|
1. synchronized | 基于 JVM 的 Monitor 锁,通过对象头实现锁状态管理 | - 自动加锁/解锁 - 支持锁升级(偏向锁→轻量级锁→重量级锁) - 非公平锁 | 简单的代码块或方法同步,低竞争场景 |
2. ReentrantLock | 基于 AQS(AbstractQueuedSynchronizer)实现 | - 支持公平锁与非公平锁 - 可中断、可超时 - 支持多条件变量(Condition ) | 需要灵活控制的复杂同步场景 |
3. 原子类 | 基于 CAS(Compare-And-Swap)无锁算法 | - 无阻塞、高并发性能 - 仅适用于单一变量操作 | 高并发计数、状态标记等简单原子操作 |
4. 读写锁(ReentrantReadWriteLock ) | 分离读锁(共享)和写锁(独占) | - 读操作并发度高 - 写操作互斥 | 读多写少场景(如缓存) |
synchronized vs
ReentrantLock
维度 | synchronized | ReentrantLock |
---|---|---|
锁获取方式 | 自动获取/释放,无需手动管理 | 需手动 lock() 和 unlock() |
公平性 | 仅支持非公平锁 | 支持公平锁与非公平锁 |
功能扩展 | 不支持超时、中断 | 支持 tryLock(timeout) 、lockInterruptibly() |
性能 | JVM 优化成熟,低竞争下性能优 | 高竞争下更灵活,但代码复杂度高 |
适用场景 | 简单同步需求(如方法同步) | 需要精细控制的场景(如死锁恢复、条件等待) |
选择建议:
-
优先使用
synchronized
:代码简洁,JVM 自动优化锁升级,适合大多数低竞争场景。 -
选择
ReentrantLock
:需要公平锁、可中断锁或复杂条件等待时(如生产者-消费者模型)。
原子类 vs 锁机制
维度 | 原子类(CAS) | 锁机制(synchronized/ReentrantLock) |
---|---|---|
并发性能 | 无锁,高吞吐量(适合高频短操作) | 有锁,线程阻塞可能引发上下文切换开销 |
适用操作 | 单一变量的原子操作(如 i++ ) | 复杂逻辑或跨多个变量的操作 |
竞争处理 | 自旋重试(可能浪费 CPU) | 线程阻塞(节省 CPU,但增加延迟) |
内存语义 | 仅保证变量操作的原子性和可见性 | 保证临界区内的原子性、可见性和有序性 |
选择建议:
-
使用原子类:单一变量的原子操作(如计数器、标志位)。
-
使用锁机制:涉及多个共享变量或复杂逻辑的同步。
读写锁 vs 普通互斥锁
维度 | 读写锁(ReentrantReadWriteLock ) | 普通互斥锁(synchronized/ReentrantLock) |
---|---|---|
并发度 | 读操作可并发,写操作互斥 | 所有操作互斥 |
适用场景 | 读多写少(如缓存、配置管理) | 读写均衡或写多读少 |
实现复杂度 | 需区分读/写锁 | 简单,统一加锁 |
选择建议:
-
使用读写锁:读操作频率远高于写操作(如热点数据缓存)。
-
使用普通锁:读写频率接近或写操作较多。