一、 观察线程不安全现象
/**
* 演示线程不安全现象
* @author Kobayashi
* @date 2022/05/10 20:10
**/
public class ThreadInsecurity {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
// COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
static final int COUNT = 100;
// 定义两个线程,分别对 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();
System.out.println(r); // 0
}
}
理论上 r 被加了1000000次,也被减了1000000次,所以应该结果为0。
但实际上却是
在线程不安全时候,代码无法百分百得到我们想要的预期结果,所以线程安全就可以保证代码100%符合预期。
二、线程安全
如果多线程环境下代码运行的结果符合我们的预期(即在单线程环境应该的结果),则说明这个程序是线程安全的。
三、 线程不安全的原因
1、站在开发者角度:
多个线程之间操作同一块数据(共享data)
并且至少有一个线程在修改这块数据
**即使在多线程代码中,也有情况不需要考虑线程安全问题。
- 几个线程之间互相没有任何数据共享,天生是线程安全的
- 几个线程之间有数据共享,但是都做读操作,没有写操作,也是线程安全的。
2、站在系统角度:
原子性
原子性我们可以理解为,一个操作或者多个操作,要么全部执行并且执行的过程中不被任何因素打断,要么就都不执行。和数据库里边的事务是一样的道理。
首先我们得知道:
对r++或者r–
1.从内存中(r代表的内存区域)把数据加载到寄存器中 LOAD_r
2.对数据加1 ADD1
3.把寄存器中的值写回内存中 STORE_r
所以可能会存在一种情况:有一个线程对 r 进行了r++操作,但是还没有写回内存的时候,另一个线程跑进来又对 r 进行了r–操作,最终写回到内存的r的值就错了!
为什么COUNT越大,出错的概率就越大?
COUNT越大,线程执行需要跨时间片的概率就越大,碰到线程调度的概率就越大,就会导致中间出错的概率提升。
可见性
可见性指的是,一个线程对共享变量值的修改,能够及时的被其他线程所看到。
java内存模型(JMM):java虚拟机规范中定义了内存模型
目的是为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台都能达到一致的并发效果。
- 线程之间的共享变量存在主内存
- 每一个线程都有自己的“工作内存”
- 当线程要读取一个共享变量时,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
- 当线程要修改一个共享变量时,会先修改工作内存中的副本,在同步回主内存中
这时候代码中就容易出现问题!
代码顺序性
程序执行的顺序按照代码的先后顺序执行
举个栗子:
一段代码,
- 去厕所刷个牙
- 回屋里喝一杯牛奶
- 去厕所照照镜子
如果单线程情况下,JVM,CPU指令集会对其进行优化,比如,会按照 1 -> 3 -> 2 的顺序执行,从结果上来看也没有什么问题,还少跑一次撤硕。这种叫做指令重排序。
重排序,就是执行指令的顺序和书写指令的顺序不一致。
JVM规定了一些重排序基本原则: happend-before原则
简单解释就是:
JVM要求无论怎么优化,对于单线程的视角,结果不会有改变。但是并没有规定多线程环境的情况,就导致在多线程情况下可能出现问题。
那么如何解决线程安全呢?
加锁
/**
* @author Kobayashi
* @date 2022/05/10 21:08
**/
public class Demo1 {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
// COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
static final int COUNT = 1000000;
// 定义两个线程,分别对 r 进行 加法 + 减法操作
// r++ 和 r-- 互斥
static class Add extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
synchronized (Demo1.class) {
r++; // r++ 是原子的
}
}
}
}
static class Sub extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
synchronized (Demo1.class) {
r--; // 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); // 0
}
}
synchronized加锁的用法还有很多种,这里只使用了其中一种。
关于线程方面的代码总结:
https://gitee.com/Z-Y-Hhhh/zyh_classcode/tree/master/src/thread
谢谢浏览