提示:以下是本篇文章正文内容,Java系列学习将会持续更新
一、什么是线程不安全
现象
线程不安全的现象就是预期结果和实际结果不一致。
预期运行结果:0
实际运行结果:随机数
/**
* 线程不安全的现象
*/
public class Demo {
// 定义一个被操作的数
static int r = 0;
// 定义操作的数
static final int COUNT = 100_0000;
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 {
Thread add = new Add();
add.start();
Thread sub = new Sub();
sub.start();
// 等待结束
add.join();
sub.join();
// 理论上还是0, 但是实际现象是吗?
System.out.println(r);
// 实际运行结果,并不是100%正确;反而随着COUNT的增大,出错概率也增大
}
}
when is 线程不安全?
对于开发者,什么情况下会出现线程不安全?
多个线程共享同一块数据,且至少有一个线程对共享数据做修改(写)操作。
在多线程的代码中,哪些情况下不需要考虑线程安全问题?
①几个线程之间互相没有任何数据共享的情况下,天生是线程安全的;
②几个线程之间即使有共享数据,但都是做读操作,没有写操作时,也是天生线程安全的。
二、造成线程不安全的原因
(1)线程的非原子性操作
程序员的预期是r++和r–是一个原子性的操作。但实际执行起来保证不了原子性。
Ⅰ. java代码(高级语言)中的一条语句,很可能对应的多条指令
r++实质就是r= r+1,变成指令动作:
①LOAD_ A // 从内存中把数据加载到寄存器中
②ADD 1 // 完成数据加1的操作并写入寄存器中
③STORE A // 把寄存器中的值写回到内存中
Ⅱ.线程调度是可能发生在任意时刻的,单看这3条指令,线程调度可能在其中穿插执行,破环了原子性。
(2)内存可见性
内存可见性问题:在多线程共享一个数据块下,一个线程对数据进行修改操作时,其它线程是无法感知的。甚至会被编译器优化到完全不可见的程度。
例如:当两个线程同时操作一个内存,例如一个读一个写,但是当“写线程”进行的修改的时候,“读线程”可能读到修改前的数据,也可能读到修改后的数据,这是不确定的。
不可见的原因:
CPU为了提高数据获取速率,会设置缓存。
在多核CPU下,每个核都有自己的独占缓存进行数据存取,只有在所有处理结束后,才会将数据同步到主存中。
所以会导致有些核读取到的是过期的数据。
(3)指令重排序
与编译器优化直接相关:
为了提高程序的执行效率,调整了执行的顺序(调整的目的是为了提高效率,不改变逻辑)
编译器也会自动对顺序进行调整。单线程下调整不会出现问题,多线程下调整会出现大问题。
说白了,就是书写指令和执行指令不一致导致的错误。
JVM规定了一些重排序的基本原则: happend-before 规则
简要的解释:
JVM要求,无论怎么优化,对于单线程的视角,结果不应该有改变。
但并没有规定多线程环境的情况(并不是不想规定,而是不能规定)
导致在多线程环境下可能出问题
总结:
提示:这里对文章进行总结:
以上就是今天的学习内容,本文是Java多线程的学习,介绍了线程不安全的现象,以及造成线程不安全的主要原因。之后的学习内容将持续更新!!!