java基础一.volatile
被volatile修改的变量进行写操作的时候,会立即刷新近主存。读的时候,直接去主存中读取。
要说volatile,先简单的说一下java的内存模型。
首先,我先去百度张图片。
java中,jvm会给每个线程分配一个独立的内存空间,用于存放本线程的局部变量等,还有一部分空间叫做共享变量副本,这个部分空间存放的是从主存中复制过来的共享变量。目的是为了加快CPU的处理速度。
在这种前提下,并发环境中就很容易出现问题。
假设:
int i=0;
i++;
i++并非是原子操作。先读取到i,然后对i进行+1操作,然后将I+1后的值赋给i。
当线程A刚刚读取到i,线程B横插一足,读取i,i+1,复制给i。这时候(在线程B眼中)i=1,此时,线程A继续执行i+1=1,赋值给i,这时候i仍然是i。而使用了volatile就不会了。这就涉及到了volatile关键字的性质。
volatile有两个性质:
1.可见性:被volatile修改的变量,多个线程修改了其值,其他线程都能立即看到修改后的值。
2.有序性:对于被volatile修饰的变量,JVM会禁止对其指令重排序。
指令重排序是JVM为了加快运行速度,可能对程序的代码进行优化,它不保证程序执行时候的代码顺序和所书写的代码顺序是一致的,但是它保证程序执行完毕的结果和代码顺序执行的结果是一致的(结果一致,过程不同)。
volatile有序性:表现在两个方面:
1.程序执行的时候:被volatile修饰的变量,前面的语句都执行完毕后,才会执行本语句。同样在volatile后面的语句,只有在volatile修改的变量语句之后,才会被执行。
2.指令优化的时候:不能将volatile修饰的变量放到某些语句之后,也不能volatile修饰的变量语句后面的放到volatile之前。
volatile并不能保证原子性:
public
class
Test {
public
volatile
int
inc =
0
;
public
void
increase() {
inc++;
}
public
static
void
main(String[] args) {
final
Test test =
new
Test();
for
(
int
i=
0
;i<
10
;i++){
new
Thread(){
public
void
run() {
for
(
int
j=
0
;j<
1000
;j++)
test.increase();
};
}.start();
}
while
(Thread.activeCount()>
1
)
//保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
|
/*******************************************Copy内容****************************************************************************/
volatile的实现原理
1.可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。
2.有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
/***********************************************************************************************************************/什么时候会用到volatile关键字呢?
1.对变量的写不依赖于当前值(volatile不能保证原子性的结论下)
2.该变量没有包含在具有其他变量的不变式中。
常用于:状态标记量。单例模式中的double check。
public class Demo2SingleTon { private volatile static Demo2SingleTon singleTon; private Demo2SingleTon(){ } /** * 双重校验锁的方式 * 这种方式,只是会在创建的对象的时候加锁,也就是第一次创建对象的时候加锁。 * @return */ public static Demo2SingleTon getInstance(){ if(null!=singleTon){ synchronized (singleTon){ if(null!=singleTon) { singleTon = new Demo2SingleTon(); } } } return singleTon; } } |
instance = new Demo2SingleTon() 这句,实际上做了三件事:
1.给singleTon分配内存。
2.调用SingleTon的构造函数来初始化成员变量。
3.将instance对象指向分配的内存空间。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
2018-03-21 方小白 畅游大厦