一、线程安全的概念
指的是多个线程访问同一资源时,不会引发数据不一致或其他不可预期的行为(也就是bug),且不需要额外同步机制。换句话说就是,线程安全的代码在并发执行时能够正确运行,并且确保数据的一致性。
二、线程安全产生的原因
1.随机调度,抢占式执行
什么是随机调度?操作系统中,多个线程会同时执行,但是CPU的核心数量是有限的,所以得按照某些策略来执行调度,让每一个线程都能按照一定的CPU执行。
什么是抢占式执行?就是优先级高的线程会抢占优先级低的线程的运行资源,先让优先级高的线程执行完自己的任务,再执行优先级低的线程的任务。
2.多个线程同时修改一个变量
在多线程中,如果同时修改一个变量,可能就会出现以下结果:
count初始化为0,如果t1、t2线程同时取出count,对它进行++操作的话,就会出现最终结果等于1,也就是只进行了一次++操作的结果,那是因为线程执行是并发执行的,所以他们并不一定会一个线程执行完后再执行下一个线程,他们同时执行时,取值都是0,所以看似俩次操作++,实则只是对0进行了一次++操作。
3.原子性
原子性是指操作的不可分割性,指某个操作是必须得完整的执行完,才能继续执行下一次操作。如下例:
这里能看到,我们的初衷是让俩个线程进行自加,使得count变量自加1万次,预期结果应该是count=10000,但是这里出现的结果和我们预期的不一样,这是为什么呢?其实就是因为我们没有确保count的原子性,使得它自加的时候出现了问题,如:
这里是因为count++这一步出现了问题,因为寄存器如果要从内存上++一个数据的话是要执行三条指令的,load是从内存中读取数据到寄存器中,add是在寄存器中进行count++,save是将寄存器中的数据读回内存中,因为++这个操作分为了三步,所以这里的数据就没有保持了原子性,导致线程穿插执行,使得结果不一致。
4.内存可见性
内存可见性是指一个线程对共享变量修改能够被其他线程及时、正确看到。内存可见性问题也就是当多个线程对共享变量进行修改时,线程A对共享变量修改在线程B不可见。
产生的原因:
- 线程缓存:每个线程都有自己的缓存,当他们修改共享变量时,是从本地缓存中读取的,而不是从主内存中读取。这种情况下就会出现共享变量没有及时更新反映到其他的线程中。
- 重排序优化:编译器和处理器为了提高执行效率,可能会对代码执行进行重排序(即改变操作的执行顺序)。这可能导致某些操作的顺序与程序代码中的顺序不一致,从而影响内存可见性,就比如下图,重排序后原来的顺序乱了:
四、如何解决线程安全问题
1.synchronized关键字
在解决线程安全问题里面,我们就可以使用到synchronized关键字。
这里我们将线程用上锁之后,发现count就能够按照我们的预期自加到10000,这就是因为我们用上了synchronized,怎么定义锁呢,就是实例化出一个Object的对象,将其放到synchronized的形参里面,就行成了一把锁。
synchronized主要有俩个特性,第一个特性是互斥,第二个特性是可重入;互斥,顾名思义就是不能放一起,当某个线程在执行某个对象使用synchronized的时候,其他线程如果先要用到同一个synchronized,必须要阻塞等待前一个使用完,这也是我们所说的锁竞争。
可重入也就是同一操作,你及时用多把锁锁住也不会有问题,如以下代码:
public class demo4 {
static int count;
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
Thread t1=new Thread(()->{
synchronized(locker){
synchronized(locker){
for(int i=0;i<5000;i++){
count++;
}
System.out.println("线程t1结束");
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
synchronized (locker){
for(int i=0;i<5000;i++){
count++;
}
System.out.println("线程t2结束");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
}
2.volatile关键字
volatile 修饰的变量, 能够保证 "内存可见性"
代码在写入 volatile 修饰的变量的时候,
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本