本编文章都是基于下图这个,计算机cpu 、缓存、内存、线程之间的关系;
一、缓存一致性问题
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
中间的高速缓存就是cpu和内存的中间过程。
但是在多线程,每个线程在不同的cpu中运行时,每个线程分别读取内存中的值存入各自所在的CPU的高速缓存当中,cpu对数据改变后,就造成了缓存一致性的问题,通常称这种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:
1)通过synchronized锁的方式
2)通过缓存一致性协议
二、并发编程中的三个概念
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,相当于事物的概念。
可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行,因为有指令重排序问题;
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。这种情况在单线程下没有问题,但是在多线程下有可能出现问题。
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
三、synchronized
同步锁可以保证并发编程中的原子性、可见性、有序性;但是效率比较低。
四、volatile
-
保证可见性问题:
一个共享变量被volatile修饰时,当CPU对该变量有写操作时,它会保证修改的值会立即被更新到主内存中,并会发出信号通知其他CPU将该变量的缓存设置为无效状态,因此当其他CPU需要读取这个变量时,发现自己的高速缓存中该变量是无效的,那么它就会从内存重新读取。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
可见性只能保证每次读取的是最新的值
-
保证有序性问题:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
不能保证原子性问题:
测试代码:
package com.test.jvm;
public class Test {
public volatile int i = 0;
public void increase(){ //可以添加关键字synchronized看结果不同
i++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int x =0; x<10; x++){
new Thread(){@Override
public void run() {
for(int y=0; y<1000; y++){
test.increase();
}
}
}.start();
}
while(Thread.activeCount()>1){ //保证前面的线程都执行完
Thread.yield();
System.out.println(test.i);
}
}
}
最后i的结果并不是10000 ,总是小于10000;
解释:
假如某个时刻变量 i 的值为10,
cpu1中线程A对变量进行自增操作,线程A先读取了变量 i 的原始值,然后线程A被阻塞了;
然后cpu2中线程B对变量进行自增操作,线程B也去读取变量 i 的原始值,由于线程A只是对变量 i 进行读取操作,而没有对变量进行修改操作,所以不会导致线程B的工作内存中缓存变量 i 的缓存无效,所以线程B中 i 的值10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程A接着进行加1操作,由于已经读取了 i 的值,注意此时在线程A的工作内存中 i
的值仍然为10,所以线程A对 i 进行加1操作后 i 的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,i 只增加了1。
解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。但是要注意,线程A对变量进行读取操作之后,被阻塞了的话,并没有对 i 值进行修改。然后虽然volatile能保证线程B对变量 i 的值读取是从内存中读取的,但是线程A没有进行修改,所以线程B根本就不会看到修改的值。
总结:
使用volatitle关键字要保证的两个条件:
1) 对变量的写操作不依赖于当前值
2) 该变量没有包含在其他变量中