一、概念
线程不安全是指程序在多线程的执行环境下,程雪的执行结果不符合预期。
二、举例说明(首先我们先来观察一下单线程的执行结果)
1.示例:
在这个程序中,我将++操作和--操作分别针对于一个变量执行了相同多次,我的本意是希望我的程序最总的输出结果能是变量最开始的值,没加也没减。但单线程和多线程会得到不一样的结果,居易见下述执行结果以及结论。
2.执行代码
package thread.threaddemo;
/**
* @Author: wenjingyuan
* @Date: 2022/11/12/21:20
* @Description:单线程下程序的执行结果
*/
public class ThreadDemo16 {
//定义一个内部类
static class Counter{
private static int number=0;
private int MAX_Counter=0;
public void incr(){
for (int i = 0; i < MAX_Counter; i++) {
number++;
}
}
public void decr(){
for (int i = 0; i < MAX_Counter; i++) {
number--;
}
}
public Counter(int MAX_Counter){
this.MAX_Counter=MAX_Counter;
}
}
public static void main(String[] args) {
Counter counter=new Counter(100000);
counter.incr();
counter.decr();
System.out.println("最终结果为:"+counter.number);
}
}
3.执行结果:
4.结论
我们通过最终的结果可以看出来,在单线程的情况下,我们可以得到我们想要的结果,因为我的程序总是在加法操作执行结束以后再去执行所谓的减法操作。
三、举例说明(其次我们来观察一下多线程部分的情况)
1.执行代码
package thread.threaddemo;
/**
* @Author: wenjingyuan
* @Date: 2022/11/12/21:20
* @Description:单线程下程序的执行结果
*/
public class ThreadDemo16 {
//定义一个内部类
static class Counter{
private static int number=0;
private int MAX_Counter=0;
public void incr(){
for (int i = 0; i < MAX_Counter; i++) {
number++;
}
}
public void decr(){
for (int i = 0; i < MAX_Counter; i++) {
number--;
}
}
public Counter(int MAX_Counter){
this.MAX_Counter=MAX_Counter;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter(100000);
// counter.incr();
// counter.decr();
//我们尝试着去将这个操作放在两个线程里边去执行
Thread t1=new Thread(()->{
counter.incr();
});
Thread t2=new Thread(()->{
counter.decr();
});
//启动两个线程
t1.start();
t2.start();
//等待两个线程执行结束以后
t1.join();
t2.join();
System.out.println("最终结果为:"+counter.number);
}
}
2.执行结果
第一次:
第二次:
3.结论
我们通过执行多次结果会发现,每次的执行结果都不一样,跟我们所想要的结果是有误差的,那么这就是多线程带来的不安全问题。
四、线程安全性问题是如何导致的?
- 抢占式执行(首要原因,狼多肉少)
- 多个线程同时修改了同一个变量
- 操作是非原子性操作
- 内存可见性问题
- 指令重排序
接下来我来对于后三点仔细解释一下:
③ 非原子性操作
什么是原⼦性?
我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊ 房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。⼀条 java 语句不⼀定是原⼦的,也不⼀定只是⼀条指令。
⽐如刚才我们看到的 n++,其实是由三步操作组成的:1. 从内存把数据读到 CPU2. 进⾏数据更新3. 把数据写回到 CPU
不保证原⼦性会给多线程带来什么问题?如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是 错误的。
用一幅图再来解释一下:
④ 内存可⻅性
可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到。Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀ 致的并发效果.下面我们来看Java内存模型的图片 。![]()
- 线程之间的共享变量存在主内存 (Main Memory).
- 每⼀个线程都有⾃⼰的 "⼯作内存" (Working Memory) .
- 当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷⻉到⼯作内存, 再从⼯作内存读取 数据.
- 当线程要修改⼀个共享变量的时候, 也会先修改⼯作内存中的副本, 再同步回主内存.
由于每个线程有⾃⼰的⼯作内存, 这些⼯作内存中的内容相当于同⼀个共享变量的 "副本". 此时修改线程1 的⼯作内存中的值, 线程 2 的⼯作内存不⼀定会及时变化。
⑤ 指令重排序
编译器优化的本质是调整代码的执⾏顺序,在单线程下没问题,但在多线程下容易出现混乱,从⽽造成线程安全问题。