文章目录
- 前言
- 线程不安全的5大原因
- 1. 抢占式执行和随机调度
- 2. 多个线程同时修改一个变量(共享数据)
- 3. 修改操作不是原子性的
- 4. 内存可见性
- 5. 指令重排序
前言
什么是线程安全?
简单来说,如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
但是在多线程的环境下,我们很难预知线程的调度方法,这就像A和B两个施工队,同时在山的两头开始挖隧道,理想状态下我们希望他们能够在中间相遇,但是也极有可能他们没有相遇,各自挖了一条隧道。
提示:以下是本篇文章正文内容,下面案例可供参考
线程不安全的5大原因
1. 抢占式执行和随机调度
线程的抢占式执行
- 是指操作系统可以在任何时刻强制暂停当前线程的执行,并将处理器分配给另一个就绪状态的线程。抢占式执行可以保证操作系统的响应能力和调度公平性,避免某个线程长时间占用处理器而导致其他线程无法得到执行的问题。
线程的随机调度
- 是指操作系统在多个就绪状态的线程中随机选择一个线程来执行。随机调度可以保证线程执行的公平性和可预测性,避免某个线程过度优先导致其他线程无法得到执行的问题。
简单来说就是,线程中的代码执行到任意的一行,都随时可能被切换出去。
代码示例
public class Ceshi {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i <= 10; i++) {
System.out.print(i + " ");
}
});
Thread t2 = new Thread(() -> {
for (int i = 11; i <= 20; i++) {
System.out.print(i + " ");
}
});
//我们期望能够打印0-20的递增形式
t1.start();
t2.start();
}
}
输出结果 1
输出结果 2
输出结果 3
图解
可以看出每次执行的结束都不相同,这是由于线程的抢占式执行和随机调度的结果,你无法预测操作系统会如何安排任务的执行顺序。
2. 多个线程同时修改一个变量(共享数据)
代码示例
public class Ceshi {
public static int a = 0;//共享数据a
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
a += 2;
if (a > 0) System.out.print("大 ");
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
a -= 3;
if (a < 0) System.out.print("小 ");
}
});
t1.start();
t2.start();
//保证t线程能够执行完毕
Thread.sleep(1000);
System.out.println();
}
}
输出结果 1
输出结果 2
输出结果 3
图解
t1 和 t2 这两个线程都能够访问到a,两个线程同时分别对a进行修改和判断,你无法预料到它会被两个线程如何的互相争夺。
3. 修改操作不是原子性的
代码示例
class Counter1 {
private int count = 0;
public void add() {
count++;
// ++ 操作就不是原子性的
// 它会被操作系统分为三步操作
//1. load,把内存中的数据读取到cpu寄存器中
//2. add,把寄存器中的值,进行+1运算
//3. save,把寄存器中的值写回到内存中
}
public int get() {
return count;
}
}
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
//两个线程,分别对count++
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//与预期结果100000不同(线程不安全问题,抢占式执行)
System.out.println(counter.get());
}
}
输出结果
图解
4. 内存可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到
如果a不是内存可见的,那么t1和t2就会同时对a进行修改,可能会对预期的结果产生问题。
5. 指令重排序
指令重排序是现代处理器为了提高指令执行效率所采取的一种优化手段。它可以将指令的执行顺序进行重新排序,以最大程度地利用CPU内部资源,提高CPU的执行效率。
具体来说,指令重排序可以分为两种类型:
-
编译器重排序:编译器会将乱序的代码重新排列成一个顺序执行代码,以提高程序执行速度。
-
处理器重排序:处理器会按照一定的策略对指令执行顺序进行调整,以最大程度地利用CPU内部的各种功能单元。
代码示例
public class Ceshi {
public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
// == 符号分为两部
//1. load,开销是cmp的几千倍
//2. cmp,很快
//load之前,已经cmp了很多次,编译器就认为每次load的值都相同,为了节省时间,所以直接将load优化掉了
//就导致结果出错
//一般单线程都是正确的,多线程会出错
// 空循环,会快速的执行,发现每次flag==0,此时编译器就会动了优化的心思
}
//导致该句,输出不来
System.out.println("循环结束,t1结束");
});
Thread t2 = new Thread(() -> {
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数改变flag:");
//输入的这点时间内,flag==0可能已经比较了无数次
flag = scanner.nextInt();
}
});
t1.start();
t2.start();
}
}
输出结果
可以看出,即使输入了多个数,也没有输出 循环结束,t1结束这条语句,因为编译器已经认为我比较了那么多次(由于它很快,在输入之前已经比较了无数次),flag就是固定值0,不会变了,以后就不用执行load了。
- 所以虽然指令重排序可以提高CPU的执行效率,但它也可能会带来一些问题。尤其是在多线程并发执行时,指令重排序可能会导致程序执行结果出现错误,这种问题被称为“内存模型问题”。为了解决这类问题,Java 提供了一些机制,如“volatile”关键字和“synchronized”关键字,用于禁止指令重排序和保证内存可见性。