为什么需要重排序?
如果代码严格按照我们所编写的代码顺序执行,则会出现如下问题
概念
编译器重排序
处理器重排序
数据依赖性(单处理器)
三个场景
上述三个场景包含数据依赖,因此不会进行编译器和处理器的重排序
不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑
as-if-serial语义
happens-before关系
如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间就要存在happens-before关系且第一个操作的执行顺序于第二个操作之前!(不然会存在并发问题)
如果两个操作之间存在happens-before的关系但是并不代表java对两个操作的执行顺序需要按照h-b指定的顺序来执行,只要执行的结果与h-b顺序执行的结果一致。那么这种重排序就是允许的(单线程or多线程)
规则
引发的并发问题--导致数据出现可见性的问题
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: //线程1: context = loadContext(); //语句1 inited = true; //语句2
//线程2: while(!inited ){ sleep() } doSomethingwithconfig(context); 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
线程读取的一个变量可能为过时的数据,这种安全性叫做最低安全性(适用于绝大多数变量,非volatile类型修饰的double、long除外)
所以针对所有的可变的共享变量需要用同一个锁保护,这样可保证所以线程对该共享变量的修改对其他线程是可见的
volatile变量
访问时不会加锁
如何确保可见性?
两个特点的实现原理! 在 volatile 变量的赋值操作的反编译代码中,在执行了赋值操作之后加了一行:lock addl $0x0,(%esp) 这一句的意思是:给 ESP 寄存器 +0,是一个空操作,重点在 lock 上 首先 lock 的存在相当于一个内存屏障,使得重排序时,不能把后面的指令排在内存屏障之前 同时,lock 指令会将当前 CPU 的 Cache 写入内存,并无效化其他 CPU 的 Cache,相当于对 Cache 中的变量做了一次 store -> write 操作 这使得其他 CPU 可以立即看见 volatile 变量的修改,因为其他 CPU 在读取 volatile 变量前会先从主内存中读取 volatile 变量,即进行一次 read -> load 操作
应用场景
不适用的场景
多线程访问下,对变量的写入依赖当前值,例如自增
因为volatile关键字无法保证原子性
简单的说,修改volatile变量分为四步: 1)读取volatile变量到local 2)修改变量值 3)local值写回 4)插入内存屏障,即lock指令,让其他线程可见 这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。 这也就是为什么,volatile只用来保证变量可见性,但不保证原子性
该变量不会和其他变量一起纳入正确性的条件
加锁
Update your browser to view this website correctly. Update my browser now
×