Volatile关键字
zhaolengquan Lv3

为什么会出现线程不安全的问题

volatile既然是与线程安全有关的问题,那我们先来了解一下计算机在处理数据的过程中为什么会出现线程不安全的问题。

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中会涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。

为了处理这个问题,在CPU里面就有了高速缓存(Cache)的概念。当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

我举个简单的例子,比如cpu在执行下面这段代码的时候,

1
t=t+1;

会先从高速缓存中查看是否有t的值,如果有,则直接拿来使用,如果没有,则会从主存中读取,读取之后会复制一份存放在高速缓存中方便下次使用。之后cpu进行对t加1操作,然后把数据写入高速缓存,最后会把高速缓存中的数据刷新到主存中。

这一过程在单线程中运行是没问题的, 在多线程中运行就会出现问题,在多核CPU中,每个线程可能运行在不同的CPU中,每个CPU都有自己的高速缓存,这时就会出现同一个变量在两个高速缓存中的值不一致的问题了.

假设此时t的值为0,两个线程同时读取了t的值,并存到了各自的高速缓存中,线程1对t进行了加1的操作,此时t的值为1,并且把t的值写回主内存,,但是线程2中高速缓存的值还是0,进行加1操作后,t的值还是1 然后把t写回主内存.此时两个线程都对t的值进行了+1操作,但是主内存中的值不是2 这就出现了数据丢失的问题,线程不安全.

java中线程安全问题

java语言在处理线程安全问题的时候,会有自己的处理机制,比如synchronized关键字, volatile关键字

java内存模型规定所有的变量都是存在主内存中,每个线程又都有自己的工作内存(高速缓存),线程对数据的所有操作必须在自己的工作内存中进行,而不是直接在主存中操作.并且每个线程不能访问其他线程的工作内存.java中的每个线程都有自己的工作空间,因此多个线程在处理一个共享变量的时候,就会出现安全问题.

共享变量

上面举例的t就是一个共享变量,共享变量就是能够被多个线程访问到的变量.在java中共享变量包括实例变量,静态变量,数组元素, 他们都被存放在堆内存中

volatile关键字

可见性

可见性的意思就是在多线程环境下,一个变量如果被一个 线程修改,其他线程能够立即知道这个线程被修改了,当线程读取这个变量的时候,去主内存中读取而不是自己的工作内存.

上面的例子中线程2读取变量时从主内存中读取,就能读到最新的线程1修改后的值,然后再进行+1操作,这样两边的数据就一致了.

如果一个变量被声明为volatile,那么这个变量就具有了可见性的特征,这就是volatile的作用之一

缓存一致性协议

线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在

其他处理器的操作情况,一旦嗅探到某处处理器打算修改其内存地址中的值,而该内存地址刚好也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址的无效。所以当该处理器要访问该数据的时候,由于发现自己缓存的数据无效了,就会去主存中访问。

有序性

实际上,当我们把代码写好之后,虚拟机不一定会按照我们写的代码的顺序来执行,例如下面两行代码

1
2
int a = 1;
int b = 2;

对于这两句代码,你会发现无论是先执行a = 1还是执行b = 2,都不会对a,b最终的值造成影响。所以虚拟机在编译的时候,是有可能把他们进行重排序的。

为什么要重排序呢,

假如执行int a=1 需要100ms的时间,执行int b=2需要1ms的时间,并且这两行代码并没有依赖关系,最终不会对ab的值造成影响,那肯定先执行b=2这行代码了.

volatile真的能保证一个变量的线程安全吗

我们通过上面的讲解,发现volatile关键字还是挺有用的,不但能够保证变量的可见性,还能保证代码的有序性。
那么,它真的能够保证一个变量在多线程环境下都能被正确的使用吗?
答案是否定的。原因是因为Java里面的运算并非是原子操作

原子操作

原子操作:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
也就是说,处理器要嘛把这组操作全部执行完,中间不允许被其他操作所打断,要嘛这组操作不要执行。
刚才说Java里面的运行并非是原子操作。我举个例子,例如这句代码

1
int a=b+1;

处理器在处理代码的时候,需要处理以下三个操作:

  1. 从内存中读取b的值。
  2. 进行a = b + 1这个运算
  3. 把a的值写回到内存中

而这三个操作处理器是不一定就会连续执行的,有可能执行了第一个操作之后,处理器就跑去执行别的操作的。

volatile无法保证原子性的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
public class test4 {
private static volatile int i = 0;
public static void main(String[] args) {
for (int j = 0; j < 10; j++) {
new Thread(() -> {
for (int k = 0; k < 1000; k++) {
i = i + 1;
}
}).start();
}
System.out.println(i);
}
}

image-20220315101046753

例如:
线程1读取了t的值,假如t = 0。之后线程2读取了t的值,此时t = 0。
然后线程1执行了加1的操作,此时t = 1。但是这个时候,处理器还没有把t = 1的值写回主存中。这个时候处理器跑去执行线程2,注意,刚才线程2已经读取了t的值,所以这个时候并不会再去读取t的值了,所以此时t的值还是0,然后线程2执行了对t的加1操作,此时t =1 。
这个时候,就出现了线程安全问题了,两个线程都对t执行了加1操作,但t的值却是1。所以说,volatile关键字并不一定能够保证变量的安全性。

什么情况下volatile能够保证线程安全

刚才虽然说,volatile关键字不一定能够保证线程安全的问题,其实,在大多数情况下volatile还是可以保证变量的线程安全问题的。所以,在满足以下两个条件的情况下,volatile就能保证变量的线程安全问题:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他状态变量共同参与不变约束。
 Comments