并发编程的三大特性
可见性
原子性
有序性
一个问题引发的思考
public class VolatileDemo {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
int i = 0;
while (flag){
i++;
}
});
thread.start();
Thread.sleep(1000);
flag = false;
}
}
执行上面一段代码,会发现虽然把flag变量改成了false,但是线程并没有停止,貌似main线程更改了值,对于thread线程来说并不知道,这就是我们常说的线程中的可见性问题,也是引起线程安全问题的根本原因
那怎么解决这个问题呢?非常简单,java中最常见的就是通过volatile关键字解决,如下代码:
public class VolatileDemo {
// 添加volatile关键字,解决可见性问题
public volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
int i = 0;
while (flag){
i++;
}
});
thread.start();
Thread.sleep(1000);
flag = false;
}
}
什么是可见性问题?
在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。
但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是所谓的可见性问题
为什么会出现可见性问题呢?
硬件层面
CPU/内存/IO设备,由于运行速度的差异,所以对cpu有一定的优化
主要体现在三个方面:
1.CPU层面增加了高速缓存
2.操作系统,进程、线程、| CPU时间片来切换
3.编译器的优化 ,更合理的利用CPU的高速缓存
CPU高速缓存
因为高速缓存的存在,会导致一个缓存一致性问题。 下图是CPU高速缓存的一个模型图,我们可以分析出,ThreadA线程不能及时读取到ThreadB线程更改的值(数据不可见性),从而导致缓存数据不一致
什么是有序性问题?
好,我们分析了上面例子可见性的问题,再继续看下面的例子:
public class VolatileDemo02 {
public static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
Map<String,Integer> map = new HashMap<>();
for(int i=0;i<100000;i++){
x=0;
y=0;
Thread one = new Thread(()->{
int a = y;
x = 1;
map.put("a",a);
});
Thread two = new Thread(()->{
int b = x;
y = 1;
map.put("b",b);
});
one.start();
two.start();
one.join();
two.join();
System.out.println("a=" + map.get("a")+", "+"b=" + map.get("b"));
}
}
}
打印结果按不同结果总结(可自行尝试),如下:
a=0, b=1
a=1, b=0
a=1, b=1
a=0, b=0
Process finished with exit code 0
仔细看打印结果,发现,a和b的值会有这么多不一样的情况呢?
这就是我们说的指令重排序导致的有序性问题
为什么会有指令重排序的问题呢
其实这个是CPU优化的过程中产生的一个问题
可以参考缓存一致性协议来看有序性问题的原因和解决方案,这里不再多说
volatile是怎么解决可见性和有序性的呢?
1.我们先从字节码指令层面看看添加volatile关键字前后指令有什么变化
加锁前指令:
0x00000000027cb63e: mov byte ptr [rsi+60h],0h ;*putstatic flag
; - com.xufk.thread.VolatileDemo::main@24 (line 18)
加volatile关键字后指令:
0x00000000029c4687: lock add dword ptr [rsp],0h ;*putstatic flag
; - com.xufk.thread.VolatileDemo::main@24 (line 18)
你会发现加了volatile后,字节码指令里面加了一个lock的命令,我们可以想象,这肯定跟锁有关系,没错就是通过锁实现的。
2.我们然后看volatile的在hotspot的源码实现
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (tos_type == itos) {
obj->release_int_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == atos) {
VERIFY_OOP(STACK_OBJECT(-1));
obj->release_obj_field_put(field_offset, STACK_OBJECT(-1)); OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj>>CardTableModRefBS::card_shift], 0);
} else if (tos_type == btos) {
obj->release_byte_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == ltos) {
obj->release_long_field_put(field_offset, STACK_LONG(-1));
} else if (tos_type == ctos) {
obj->release_char_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == stos) {
obj->release_short_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == ftos) {
obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
} else {
obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
}
OrderAccess::storeload();
}
我们可以看到 OrderAccess::storeload();这句代码,这是加了内存屏障实现的,解决指令重排序问题
其实volatile的实现原理,必须要先去学习Java内存模型(JMM),了解清楚多线程共享内存和工作内存之间数据是怎样交互的之后,就会对原理更加清楚了
总结
可见性:总线锁和缓存锁,缓存锁是基于缓存一致性协议来实现。
有序性:内存屏障指令
volatile解决了并发编程的可见性和有序性问题,但是并没有解决原子性问题,原子性问题还得以来synchronize这种锁来实现
相关链接:
Java内存模型(JMM)
缓存一致性协议
内存屏障
本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:1354518382@qq.com 博客地址:https://blog.csdn.net/qq_35576976/