线程安全的根本原因: 线程之间的抢占式执行, 随机调度
线程安全问题示例
最简单的, 两个线程对同一个变量各增加 5000 次, 最终输出该变量, 结果并不是我们理想的 增加了 10000 次
class Counter { public int count; public void add() { count++; } } public class Main{ public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) counter.add(); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) counter.add(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
运行结果:
可以看出, 多次运行的结果都不一样
线程不安全的原因
一般来说, 线程不安全有以下几种原因:
- [根本原因] 抢占式执行, 随机调度
- 代码结构: 多个线程同时修改同一个变量
- 原子性, 如果修改操作是原子的, 一般不会有线程安全问题, 如果是非原子的,多线程环境中出 bug 的概率就很高
- 内存可见性
- 指令重排序 (本质是编译器的优化出 bug 了)
对于上述代码而言, 线程不安全是因为 ++ 操作不是原子的,以及线程之间的调度顺序问题
++ 操作本质上要分成三步
- 把内存中的值读取到 CPU 的寄存器中 – load
- 把 CPU 寄存器的数值进行 +1 运算 – add
- 把得到的结果写回到内存中 – save
如果每个线程中的这三步都能顺序执行, 中间不被其他线程所影响, 自然没有线程安全问题 (也就和单线程没有区别了)
如果是当前线程的三个步骤中间穿插着其他线程的一些操作, 就有线程安全问题了 (这里只给出两例, 还记得 大明湖畔(MySQL) 的 的夏雨荷(脏读)嘛, 是不是很像 …)
如何解决线程安全问题?
加锁!!! – synchronized, 其实就是把一堆非原子的操作, 通过加锁变成一个原子操作
synchronized 使用方法:
- 修饰方法
1.1修饰普通方法 (修饰的是 this)
1.2 修饰静态方法 (修饰的是类对象 ) - 修饰代码块 (显示/手动指定锁对象)
sychronized 对 “对象” 加锁
修饰普通方法, 谁调用该方法, 就把谁加锁
修饰静态方法, 对该静态方法所在的类对象加锁
修饰代码块, 对指定的对象加锁
如果多个线程针对同一个对象加锁, 就会产生锁竞争/锁冲突, 其中一个线程获取到锁, 正常执行, 其他线程就进入阻塞状态, 等待锁资源被释放, 再重新竞争锁 (这里的操作本质上是把并行变成了串行,顺序执行就不会有线程安全问题了)
对示例代码加锁解决线程安全问题
针对 add() 方法加锁 (其实是对调用 add() 方法的对象(实例) 加锁)
synchronized public void add() { count++; }
针对代码块加锁 (对this对象加锁, 其实还是对调用该方法的对象(实例) 加锁)
public void add() { synchronized (this) { count++; } }
Java 标准库中的线程安全类
内置 synchronized 加锁的类, 相对来说安全
还有些类不涉及"修改"操作