上 并发编程一:深入理解JMM和并发三大特性( 三 )


什么时候工作内存的falg会没有呢?
线程中的缓存是有时间限制的,内存空间本身就比较小,如果数据多了不够放了自然会淘汰之前的缓存 。或者在while里面执行了1毫秒,才用到这个falg,那么这个falg就会被淘汰 。就是说缓存淘汰时间不到1毫秒 。或者一些其他操作也会导致缓存淘汰 。这涉及到缓存淘汰的机制和硬件层面有关,这就超纲了,知道有这么回事就好 。
方案解析
方案一解析:为什么加上 能够跳出循环
首先是java的关键字,要找原因要去看jvm的源码 。
在的实现
这串代码意思是判断是不是修饰的,如果是执行::(),::()这个意思是加上内存屏障 。这个内存屏障是jvm层面,不要跟其他搞混了 。jvm的内存屏障主要有四种:
::()在linux系统x86中的实现
inline void OrderAccess::storeload(){ fence(); }inline void OrderAccess::fence() {if (os::is_MP()) {// always use locked addl since mfence is sometimes expensive#ifdef AMD64__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");#else__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");#endif}
实际上::()调用了fence()方法 。首先这个fence()方法会先判断处理器是不是多核的,如果是单核的不会出现可见性问题 。如果是多核的 会加上 ("lock; addl $0,0(%%rsp)这一串代码,这是汇编层面的指令 。可以叫做lock前缀指令也可以理解为内存屏障的意思 。汇编指令就和硬件有关了 。来看下lock前缀指令的作用
确保后续指令执行的原子性 。在及之前的处理器中,带有lock前缀的指令在执 行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很 大 。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低 lock前缀指令的执行开销 。LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序 。LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store 中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新 store 的操作会导致其他cache中的副本失效 。
概念有点多,来总结一下 。也即是在jvm的实现中会调用::(),::()调用fence() 。fence()存在lock前缀指令 。这lock前缀指令把B线程修改后的falg值立即写入到主内存,并且让其他副本也即是A线程中的falg缓存失效,那么A线程就会从主线程获取flag的值 。
方案二和方案一相同 。反正就是让B线程flag的操作完成立即store、write写入主内存 。并且让A线程的flag失效 。
方案三使用内存屏障和LOCK前缀指令效果一样 。内存屏障这里先了解下后续会深入理解
方案四解析:为什么使用.yield() 能够跳出循环
因为.yield() 会释放时间片,释放时间片就会有上下文切换,上下文切换是这样的,A线程在执行while循环flag为false,在切换上下文之前会先保存这个时间片的产生的值,就是同步回主内存 。在下一次的轮到A线程执行的时候会从主内存总获取上下文的值继续执行 。由于JMM规定了:不允许一个线程无原因地(没有发生过任何操作)把数据从工作内存同步回主内存中 。所以在A线程上下文切换时flag不会同步回主内存 。并且上下文切换大概需要几毫秒的时间,所以A线程中的flag早就被淘汰了,下一次会直接读取主内存的flag的值,由于方法修改了flag的值并且刷新到主内存当中,所以主内存falg在方法结束后就已经是false了 。这就是.yield()能过跳出循环的原因
方案五解析:为什么使用使用.out.()能够跳出循环