并发可能会引起数据污染究竟是为什么呢(并发导致数据错误)
导语:并发可能会引起数据污染究竟是为什么?
java并发程序都是基于多线程,操作系统为了充分利用CPU的资源,将CPU分成若干个时间片,线程会被操作系统调度进行任务切换,达到最大限度的利用CPU空闲时间。
多线程编程虽然可以最大限度的利用CPU,但也突显了三个问题:原子性问题,可见性问题,有序性问题。这三个问题导致数据与期望的不一致,也就是脏数据的由来。
原子性原子性指的是一个或者多个操作在CPU执行过程中不被中断的特性。即要不全部执行完成,要不不执行。
请看上面的代码块,我们来分析下操作2:
指令1:把变量a从内存加载到CPU寄存器中;指令2:在寄存器中执行+1操作;指令3:将结果写入内存。通过上面的三步指令操作,完成了“a++”的操作。
假设线程A在完成“指令1”后切换到线程B,线程B完成操作2后切换到线程A,A继续完成指令2和指令3的步骤,我们发现两个线程都执行了“a++”的操作,但是结果却不是我们期望的2,而是1。
这就是原子性的问题,为了解决这个问题,我们需要用到互斥来使操作变为原子性。方法为:使用锁或者使用原子函数。
可见性可见性指的是当一个线程修改了共享变量后,其他线程能够立即得到修改的值。
同样是上图中的代码块,我们做下规定:操作1和操作2是线程A执行的代码;操作3是线程B执行的代码。
假设执行线程A的是CPU1,执行线程B的是CPU2。当线程A执行“a++”这句的时候,会先把a的初始值加载到CPU1的寄存器中,然后自增1,那么CPU1中的寄存器中a的值变为1,却没有立即写入内存中。
此时线程B执行操作3“b=a”代码,它会去内存中读取a的值,此时内存中i的值还是0,那么完成操作3后,b的值为0,而不是1。
这就是可见性的问题,线程A对变量a修改后,线程B没有立即看到线程A修改的值。为了解决这个问题,我们设置变量为volatile可见性。volatile的特殊规则保证了新值能立即同步到内存中,已经没使用前立即从内存刷新数据。
有序性有序性指的是程序执行的顺序按照代码的先后顺序执行。
一般来说,处理器为了提高程序的的运行效率,可能会对输入代码进行优化,它不保证程序中各个操作语句的执行先后数据同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行结果的一致性。
比如上图所示,两种顺序都有可能发生,不过不会影响代码执行结果的一致性,因为重排序会考虑指令之间的依赖,所以不会出现操作3,在操作4后面的情况。
虽然重排序不会影响单个线程内程序的执行结果,但是多线程呢?
上图代码块中,由于操作1和操作2没有数据依赖,因此可能会被重排序。假如发生重排序,线程A执行过程中操作2发生在操作1之前,而此时线程B以为初始化工作完成,那么就会跳出while循环,去执行doSomething(context)方法,而此时context并没有被初始化,就会导致程序出错。
为了解决这个问题,我们可以使用synchronized和volatile来保证多线程之间操作的有序性。volatile关键字会禁止指令重排;synchronized关键字保证同一时刻只允许一条线程操作。
本文内容由小茜整理编辑!