线程安全 — 场景、解决、悲观锁、乐观锁
线程安全 — 场景、解决、悲观锁、乐观锁
线程不安全的情景
- 多个线程**同时复合操作(先读取后修改,先检查后执行)**共享对象(静态对象或单例全局对象)。
- 对于局部对象,只有在其作为返回值、引用静态变量或创建子线程去修改时才可能会导致线程不安全,关键是看该对象有没有被多个线程同时复合操作的可能性。
- 总结:所以线程不安全的关键一是看要修改的对象是否为所有线程的共享对象,二是有没有进行复合操作。开发中最常见的例子是定义一个静态的HashMap集合来维护某种映射关系,此时需要格外注意。
解决线程不安全
-
使用
volatile
修饰共享变量(可见性)和采用线程安全的集合,比如ConcurrentHashMap
、CopyOnWriteArrayList
等等。这里有个误区,以为使用了线程安全的集合就一定能保证线程安全。
以
ConcurrentHashMap
为例子,ConcurrentHashMap
底层通过CAS 和 synchronized来保证节点添加/删除时的线程安全性。但如果进行复合操作,比如说先读取键值对是否存在再修改键值对仍有线程安全问题,因为这两个操作并不是原子的,可能线程A发现键值对存在后,CPU执行权就被线程B抢占,而线程B又把该键值对删除,从而导致线程不安全。因此
ConcurrentHashMap
提供了一些原子方法,比如putIfAbsent()
、conputeIfAbsent()
-
悲观锁:使用
synchronized
或 AQS中的Lock
锁来保证共享资源被线程独占。其中可以使用**读写锁(ReentrantReadWriteLock)**有效提高性能。private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); // 获取读锁,用于读操作 private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); // 获取写锁,用于写操作
AQS与Synchronized的区别
特性 / 维度 | synchronized | AQS(AbstractQueuedSynchronizer) |
---|---|---|
类型 | 关键字 | 类库 |
底层实现 | 基于对象的 Monitor | 基于 FIFO 队列 + CAS + volatile |
锁模式 | 独占锁,非公平 | 可实现独占锁、共享锁、公平/非公平锁等 |
可中断性 | 不支持线程中断 | 支持中断(acquireInterruptibly() ),即中断正在等待锁的线程 |
是否支持条件变量 | 通过WaitSet实现(Object.wait/notify ) | 支持多个条件变量来实现等待、唤醒机制(Condition.await/signal ) |
是否可重入 | 是(可重入锁) | 取决于具体实现类(如 ReentrantLock 支持) |
适合场景 | 在竞争激烈情况下为重量级锁,性能较差,在竞争少时为轻量级锁,性能较好 | 在竞争激烈情况下,更灵活,有多种解决方案,性能更好 |
-
乐观锁:
- 定义:等到真正更新数据的时候才检查在此期间是否有其他线程修改过数据。如果检测到数据已被修改,则更新失败,即CAS。
- 实现:对数据表添加一个版本号或时间戳字段,在执行DML时判断版本号和之前查询的版本号是否相同,如果不同则让前端重试请求
- 与悲观锁比较:乐观锁虽然避免了悲观锁底层操作系统的用户态内核态的切换(阻塞/唤醒线程需要调用操作系统的原语),但性能并不是总是更优异,当线程竞争激烈时,乐观锁会频繁修改失败,导致长时间自旋,所以更适合低并发的场景。
-
转为不共享设计,比如使用
ThreadLocal
让每个线程拥有自己的变量,或者在方法中创建一个独立的变量副本(深拷贝),方法中的所有操作都基于该副本。 -
不可变对象:对象创建后状态不可修改,天然线程安全:
private final String serverUrl;
-
原子类:基于 CAS(Compare-And-Swap)实现的无锁线程安全操作:
private AtomicInteger counter = new AtomicInteger(0);