目录
前言
在使用多线程的时候,难免会出现一些问题,会产生线程安全问题,本篇文章就来简单讲述一下线程安全问题产生的原因和对应的解决方案。
1.线程安全的定义
线程安全是指当多个线程同时访问某个对象或方法时,不会出现数据不一致、逻辑错误或者意外结果的情况。即当把一个单线程执行的程序修改成一个多线程程序,产生的结果要和原来一样,如果不一样,则可以认为出现了线程安全问题。
2.线程安全问题产生的原因
造成线程不安全有各种各样的原因,但是导致线程不安全的根本原因就是线程的调度是随机的。下面还有一些常见的原因。
2.1 多个线程修改一个变量
多个线程同时访问共享资源(比如说同一个变量)可能会产生竞争状态,比如一个线程读取的是1,然后改成2,还没有保存,另一个线程也来读取,此时读取的还是1,这就会导致每次多线程执行完毕的时候,得出的结果可能都不一样。
2.2 修改操作不是原子的
就比如说对变量进行++操作,在java语句中是一行代码,实际上是三个操作:读取内存数据到CPU;对数据进行更新;然后将数据写回CPU。
在进行多线程操作的时候,可能就会因为操作顺序的抢占而产生线程安全问题
2.3 内存可见性引起的线程安全问题
下面先给出一段代码:
public class Demo17 {
private static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(flag == 0){
//do nothing
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数字:");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
我们来尝试运行一下
我们输入一个非零数字,理论上应该弹出t1线程结束和t2线程结束,我们来看结果:
我们发现并没有显示预期的结果,这是为什么?
这里就是由于”内存可见性“。
意思就是这里t2对flag进行修改,但是t1没有感知到,就是t1”没有看见“,一直认为flag还是0。
这就是内存可见性问题。
这是由于编译器优化所产生的问题,在代码编译时,编译器会进行一些优化策略,对代码进行优化,来提高程序的效率,这里是由于编译器优化出现了误判,从而导致代码的逻辑发生了改变,上面的程序就是由于此原因而出现问题,在t1的反复循环中,flag会被存进寄存器里来提高读取效率,不通过内存来读,而t2把通过内存把flag的数值变化后,t1仍然读的是原来存取到寄存器中的‘0’的值,这就是t1线程“看不见”flag的值变化的原因。
3.解决线程安全问题的方法
上述的根本原因不好解决,我们可以通过把修改操作变成”原子的“ 来解决线程安全问题
3.1 通过synchronized关键字加锁
可以通过加锁的方式将多段代码变成一个整体,让其它的线程无法进行干扰。我们通过synchronized关键字来实现锁。
比如我们对如下代码进行分析:
public class Demo14 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
可以看出这里的操作并不是原子的,所以并不能如愿获得100000的结果。
我们就需要使用synchronized关键字来进行加锁,方法如下:
package thread;
/**
* @author Wind
* @date 2025-04-13
*/
public class Demo14 {
public static int count = 0;
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
将count++操作进行加锁,在执行count++前,后台会将语句lock,在调用完count++操作后,进行unlock操作,假设当t1加锁后,t2要进行count++前,要执行加锁,此时加锁不成功,会阻塞等待,知道t1进行unlock,这样就不会导致多个线程对同一个变量进行修改。现在观看结果,可以发现结果正确:
synchronized还可以用来修饰方法,同时也能起到加锁的作用:
public class Demo14 {
static class Counter{
private int count = 0;
private Object locker = new Object();
synchronized public void add() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
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();
System.out.println("count = " + counter.count);
}
}
这里实际上就是对add方法加锁。
synchronized也可以对静态方法加锁,此时可以认为是对类对象进行加锁。
3.2 使用volatile关键字
使用volatile关键字可以解决上面所出现的内存可见性问题。
volatile关键字用于修饰变量,用于告诉编译器,这个变量是经常性变化的,使编译器不对变量进行上面内存可见性中所出现的优化。
修改后的代码(将flag用volatile修饰):
public class Demo17 {
private static volatile int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(flag == 0){
//do nothing
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数字:");
flag = scanner.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
下面看运行结果:
可以看到,程序正常结束。
此外还需要注意的是,volatile虽然可以解决内存可见性问题,但它本身不具有原子性,无法解决不是原子性的问题。
总结
以上就是对线程安全的简单介绍,希望这篇文章能帮助你更加熟练的运用多线程。