引言
一个变量是count=0,如果A、B线程同时对count++一百万次,最后count的结果会是两百万吗?

一、为什么不是两百万
1.原子性
既然提出这个问题,那么答案就不会是简单的两百万,因为在整个过程中count是共享的变量,会出现数据覆盖问题,数据覆盖是多线程并发编程中最经典的竞态条件问题,根本原因是count++不是原子操作
count++看似是一行代码,但是底层分为三步:这三步不是原子的
- 读:从内存拿到 count 当前的值
- 改:把值 +1
- 写:把新值存回内存
原子是什么?大家高中物理应该都学过原子吧,在这里就是一个不可分割、不可打断的操纵
2.覆盖数据
举个最简单的例子:count 初始 = 0
- 线程 A 读到 0
- 线程 B 也读到 0
- 线程 A 算成 1,写回去 → count=1
- 线程 B 也算成 1,写回去 → count=1
两个线程都执行了 ++,结果却只加了 1!
这就叫:线程安全问题 → 竞态条件 → 数据覆盖
3.总结
count++ 不是原子操作,多线程同时执行时,步骤会互相穿插,导致结果错误。
二、多线程
理解多线程,可以从下面几方面来认识
1. 共享资源
多个线程能同时访问的变量 / 对象,就是共享资源。比如:count、list、map、对象属性。
只要多线程改共享资源,就可能线程不安全。
2. 原子性(最重要!)
原子操作 = 不可分割、不可打断的操作
例子:
x = 1→ 原子x++→ 非原子(三步)x = y→ 非原子
非原子操作在多线程下一定不安全!
3. 可见性
一个线程改了变量,另一个线程能不能立刻看到?
CPU 有缓存,线程会把变量读到自己的缓存里。如果不刷新回主内存,别的线程看不到最新值。
4. 有序性
代码执行顺序会不会被打乱?JVM 和 CPU 会优化重排指令,单线程没事,多线程会出问题。
5.总结
要保证线程安全,必须同时满足:
- 原子性(操作不被打断)
- 可见性(修改立刻能看到)
- 有序性(指令不胡乱重排)
三、解决方案---synchronized
synchronized 是 Java 内置的锁,用来解决线程不安全。它能同时保证原子性、可见性、有序性。
1.概念
这个到底是什么?用一句话来说:加锁 → 同一时间只有一个线程能执行这段代码
就像厕所:
- 一个人进去 → 锁门
- 其他人必须排队等
- 出来 → 解锁,下一个人进
2.它是如何解决多线程问题的
对共享变量进行加锁,那么就会变成原子操作:
synchronized(锁对象) {
count++; // 这一段变成原子操作!
}
实现效果:不会发生文章开头说的数据覆盖问题,结果一定正确
- 线程 A 进来,上锁
- 其他线程全部阻塞等待
- A 执行完 读 + 改 + 写
- 解锁
- 下一个线程才能进
3.synchronized 的 3 种用法
用法一:同步代码块(最常用、最推荐)
锁粒度最小,性能最好
private Object lock = new Object();
public void add() {
synchronized (lock) { // 锁这个对象
count++;
}
}
用法二:修饰实例方法
锁的是当前对象this
public synchronized void add() {
count++;
}
用法三:修饰静态方法
所的是类对象(全局唯一)
public static synchronized void add() {
count++;
}
4.synchronized保证了什么(核心)
- 原子性:同一时间只有一个线程执行
- 可见性:解锁前会把数据刷回主内存
- 有序性:加锁解锁前后不会指令重排
它是并发的 “全能解决方案”
5.底层原理(简单理解)
- 每个对象都有一个 监视器锁 (monitor)
- 加锁就是获取 monitor
- 解锁就是释放 monitor
- JVM 自动管理,不用手动关锁
四、总结
- count++ 是非原子操作,多线程会算错
- 线程不安全根源:共享资源 + 非原子操作 + 多线程修改
- 并发三大特性:原子性、可见性、有序性
- synchronized 加锁 → 同一时间只有一个线程执行
- synchronized 能同时保证三大特性,彻底解决线程安全

9万+

被折叠的 条评论
为什么被折叠?



