前言
多线程在访问共享资源时 引发数据不一致
或程序异常的现象。称之为 线程安全问题,
正文
常见线程安全问题场景
-
竞态条件
多线程争抢同一资源时,操作结果依赖于执行顺序
,导致逻辑错误。- 场景:两线程同时对
x
进行++操作,可能导致结果少加或多加。
- 场景:两线程同时对
-
可见性问题
一个线程对共享变量修改,其他线程未察觉。-
原因:
CPU 缓存
或编译器优化
可能导致数据不同步。
怎么导致的, 人话?-
CPU 缓存 导致数据不同步
线程运行时会将变量存到 CPU缓存 中,而非直接操作主内存。如果一个线程修改了变量,但未及时刷新到主内存,其他线程读取到的仍是旧值。 -
编译器优化 导致数据不同步
编译器 和 CPU 可能为了性能 从而调整指令顺序,导致线程观察到的变量状态 (值) 不一致。
场景
语句
a = 1; flag = true;
中,另一个线程可能先观察到flag = true
,却未看到a = 1
的更新。a = 1; flag = true; // 可能被提前执行 if (flag) { System.out.println(a); } // 输出可能是 0
-
-
应对常用手段
-
synchronized - 关键字
-
为代码加锁,确保同一时间仅一个线程执行。
-
场景
public synchronized void method() { count++; }
-
-
volatile - 关键字
-
保证共享变量的 可见性,并阻止指令重排序。
-
场景
private volatile boolean flag = false;
-
-
显式锁(Lock)
-
通过
java.util.concurrent.locks.ReentrantLock
提供更灵活的锁机制。 -
基本使用:
-
lock()
:获取锁。如果锁已被占用,当前线程会阻塞。 -
unlock()
:释放锁。必须在try-finally
块中调用,以确保锁最终会被释放。 -
适合需要手动管理锁的场景,例如频繁获取和释放锁的复杂业务。
示例
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockExample { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); // 获取锁 try { count++; // 线程安全的计数操作 } finally { lock.unlock(); // 确保最终释放锁 } } public int getCount() { return count; } }
- 优点:
- 灵活性:
可以手动控制锁的获取与释放,支持公平锁与非公平锁。 - 额外功能
支持条件变量Condition
,可以实现更高级的线程协调。
- 灵活性:
- 注意事项:
- 必须确保
unlock()
被调用,即使代码中发生异常,否则会导致死锁。 - 使用不当可能增加代码复杂性,不推荐在简单场景中使用。
- 必须确保
-
-
线程安全类
-
借助
java.util.concurrent
包中的线程安全工具类,如ConcurrentHashMap
、CopyOnWriteArrayList
-
示例
// 使用 ConcurrentHashMap ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("key1", 1); map.computeIfPresent("key1", (key, value) -> value + 1); // 原子操作更新值 // 使用 CopyOnWriteArrayList CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("item1"); list.forEach(System.out::println); // 安全遍历
-
-
原子类
-
使用
java.util.concurrent.atomic
提供的原子操作类,避免复杂的同步逻辑。 -
常用方法
-
incrementAndGet()
:将当前值加1,并返回加后的值。 -
compareAndSet(expect, update)
:如果当前值等于预期值expect
,则将其更新为update
。
示例
// 使用 AtomicInteger 进行计数 AtomicInteger count = new AtomicInteger(0); // 自增操作 int newValue = count.incrementAndGet(); // 原子操作,线程安全 System.out.println("计数值:" + newValue); // Compare-and-Set 示例 boolean updated = count.compareAndSet(1, 5); // 如果当前值是 1,则更新为 5 System.out.println("是否更新成功:" + updated);
优点:
- 无需显式加锁,避免锁竞争导致的性能下降。
- 简化代码逻辑,适合计数、状态标记等高频操作场景。
-
总结 - 核心原则
- 减少共享可变状态:优先使用不可变对象。
- 使用适当同步机制:根据需求选择
synchronized
、Lock
或volatile
。 - 使用并发工具类:利用
java.util.concurrent
提供的线程安全库。
总结
线程安全问题的根源在于多线程对共享资源的非同步访问。注重合理的同步机制,才能有效保证数据一致性和程序的稳定运行。最后,感谢好学的你能浏览至此,聪明的你定能早日成为行业巨匠!
end…
如果这篇文章帮到你, 帮忙点个关注呗, 不想那那那点赞或收藏也行鸭 (。•̀ᴗ-)✧ ~
评论区欢迎讨论!
'(இ﹏இ`。)