1. 观察线程不安全
接下来我们现象演示什么是线程不安全:在代码“没有问题”的情况下,但结果是错误的(无法100%得到预期的结果)
// 演示线程不安全的现象
public class Main {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
static final int COUNT = 100000;
// 定义两个线程,分别对 r 进行 加法 + 减法操作
static class Add extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
add.start();
Sub sub = new Sub();
sub.start();
//等待两个线程结束
add.join();
sub.join();
// 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
// 所以,结果应该是 0
System.out.println(r);
}
}
发现每次的结果不一样 且没有达到预期结果
原因分析:
(1)站在开发者角度
- 多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据;
- 至少有一个线程在修改这块共享数据。
多个线程中至少有一个在对共享数据做写操作。
即使在多线程的代码中,哪些情况下不需要考虑线程安全问题呢?
- 几个线程之间互相没有任何数据共享的情况下,天生是线程安全的。
- 几个线程之间即使有共享数据,但都是读操作,没有写操作时,也是天生线程安全的。
(2)系统角度
前置知识:
- 高级语言(java代码)中的一条语句,很可能对应的时多条指令,以r++为例:r++的实质就是r = r + 1,变成指令动作:LOAD_A、ADD 1、STORE_A
- 线程调度是可能发生在任意时刻的,但不会切割指令(一条指令只有执行完/完全没有执行两种可能)。
- 单看 LOAD_A、ADD 1、STORE_A这三条指令,线程调度的发生可能会在四个时刻。
我们的预期是r ++或者r --是一个原子性的操作(全部完成或者全部没有完成),但实际执行起来,保证不了原子性,所以会出错。
在上面的例子中,我们可以发现,当设置的count值越大时,出错概率也是越大的,原因在于count越大,线程执行需要跨时间片的概率越大,导致中间出错的概率越大。
2. 线程安全的概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
3. 线程不安全的原因
3.1 原子性
++的操作,本质上是三步操作,是一个"非原子"的操作。
可以通过加锁的方式,把这个操作变成原子性。
3.2 内存可见性
前置知识:CPU为了提升数据获取速度,一般会在CPU中设置缓存(cache),因为指令的执行速度 >> 内存的读写速度。
而JVM规定了JVM内存模型来模拟这种现象:在这个规定中,把一个线程想象成一个CPU
- 主存储/主内存:是对真实内存的模拟;
- 工作存储/工作内存:是对CPU中缓存的模拟。
线程的所有数据操作(读或写)必须:
- 从主内存加载到工作内存中;
- 在工作内存中进行处理(允许在工作内存中处理很久);
- 完成最终的处理之后,再把数据同步回主内存。
一个线程对数据的操作,很可能其他线程是无法感知的,甚至某些情况下,会被编译器优化成完全看不到的结果!
可以采用synchronized
关键字对线程加锁或者使用volatile
关键字保证内存可见性!
3.3 代码重排序
指令重排序导致线程不安全问题,这也是由于编译器的优化操作而导致的线程不安全问题!我们的代码先后执行顺序有时候并不会影响我们的结果,那么这时编译器在不改变代码逻辑的基础上就会改变一下顺序,提高运行效率,而这个操作在多线程往往会出现线程不安全问题!
这里也可以使用synchronized关键字避免指令重排列!
注:JVM也规定了一些重排序的基本原则:happend-before规则。可解释为:JVM要求,无论怎么优化,对于单线程来说结果不应该有改变。但并没有规定多线程环境的情况(不能规定),导致在多线程环境下可能出现问题。
4. 小结线程安全问题
程序的线程安全:运行结果100%符合预期。
Java语境下,经常说某个类、对象是线程安全的:这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是“简单”使用,可以不考虑线程安全的问题。