线程可见性问题的分析和解决办法

可见性问题:让一个线程对共享变量的修改,能及时的他线程看到。

CPU性能优化的手段:

1、 高速缓存

Cpu与内存交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速缓冲区。但随着cpu不断发展,内存的读写速度远远不及cpu的处理速度,因此cpu厂商在每颗cpu上都加上了高速缓存(L1,L2,L3),用于缓解这种情况。Cpu结构及其与内存的交互情况大致如下:
线程可见性问题的分析和解决办法
L1: 一级缓存其实还分为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache,I-Cache,L1i),分别用于存放数据及执行数据的指令解码,两者可同时被CPU访问,减少了CPU多核心、多线程争用缓存造成的冲突,提高了处理器的效能。一般CPU的L1i和L1d具备相同的容量
L2: 二级缓存比L1一级缓存的容量要更大,但是L2的速率要更慢,为什么呢?首先L2比L1要更远离CPU核心,L1是最靠近CPU核心的缓存,CPU需要读取L2的数据从物理距离上比L1要更远;L2的容量比L1更大,打个简单的比喻,在小盒子里面找东西要比在大房间里面找要方便快捷。这里也可以看出,缓存并非越大越好,越靠近CPU核心的缓存运行速率越快越好,非最后一级缓存的缓存容量自然是够用即可
L3: L3即为L2与主内存之间的缓冲器,并且L3是多核共享的。主要体现在提升处理器大数据处理方面的性能,对游戏表现方面有较大的帮助。
运行速率:
L1>L2>L3>内存>硬盘
高速缓存的出现大大提升了系统运行速率,但也由之带来许多问题:
缓存一致性问题:
缓存中的数据与主内存的数据并不是实时同步,各cpu间内存的数据也不是实时同步。在同一个时间点,各个cpu所看到的同一内存地址的数据可能是不一致的。
针对缓存一致性问题,提出了缓存一致性协议

2、 cpu指令重排

如下图是指令重排的简单示意图:
线程可见性问题的分析和解决办法
指令重排为什么能提高cpu的效率?
以上图代码为例,
正常步骤,是先将100写入X,再读取z的值,最后将Z写入y,但是如果此时在L3被其他cpu占用也是在进行写操作,那在进行x=100的写操作时,就要先等待这个正在执行写操作执行完才能去执行写操作以及后续的其他操作。所以为了提效率,cpu进行了指令重排。
重排后,如果此时在L3被其他cpu占用进行写操作,那么另一个cpu可以先进行读操作读取z值,同时也可以等待L3被释放。以此提高cpu处理性能。但是重排序必须遵循as-if-serizal语义(单个线程指令不管怎么重排,语义不变,程序的执行结果不变)。

但是as-if-serizal语义在多线程不能保证语义一致。就会导致可见性问题。
可见性:通俗的讲,就是线程1看不到线程2写入的变量值
CPU可以导致可见性问题,但是时间很短 ,肉眼无法甄别,但是程序级别仍然会存在异常情况
如下图:单独的thread-1或者thread-2经过指令重排变没有改变程序的语义,但当thread-1、thread-2并行运行时,经过指令重排,语义便发生了改变。
重排前:运行第一行时:r2=A,r1=B,此时B还未被赋值,B的值可能是初始化的任何值
重排后:运行第一行时:B=1, r1=B,此时B=1。
线程可见性问题的分析和解决办法
带来的问题:
多核多线程情况下,指令逻辑无法分辨关联,可能出现乱序执行,导致程序结果错误。
3、 CPU性能优化带来的线程可见性问题解决办法
写内存屏障(Store Memory Barrier):在指令后插入StoreBarrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排
读内存屏障(Load Menory Barrier):在指令前插入LoadBarrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据
强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题。

JIT编译(Just In Time Compiler)

1、 JIT编译简述

JIT编译:运行时需要代码时,将 Microsoft 中间语言 (MSIL) 转换为机器码的编译。

Java语言是:解释语言+编译语言
解释执行:即脚本语言,在执行时由语言的解释器讲起一条条翻译成机器可识别的指令
编译执行:将我们编写的整段程序打包给到compiler编译器将它翻译成机器可以识别的指令码去执行
Java语言执行过程:java源代码—>执行前编译—>二级制字节码—>加载到JVM中—>解释执行或由jit编译将二进制字节码转换成机器码去执行
字节码在jvm中优先通过解释执行,将字节码一条一条翻译成机器指令去执行。当方法被多次调用或者方法中的循环体多次循环时,就会从解释执行上升到编译执行
JIT编译的进行指令重排,server模式会有指令重排优化,client模式没有
如下代码演示指令重排带了的问题
线程可见性问题的分析和解决办法
线程可见性问题的分析和解决办法
运行时64位jdk,server模式下时,迟迟不打印i的值,原因是代码经过指令重排后讲 isRunning的值缓存成了true,所以会出现这一问题
线程可见性问题的分析和解决办法

2、 JIT编译导致线程可见性问题解决办法——volatile

针对JIT编译指令重排导致的线程安全问题,可以使用volatile关键字。
线程可见性问题的分析和解决办法
线程可见性问题的分析和解决办法
JVM内存模型规定,对volatile变量v的写入,与所有其他线程后续对v的读同步。
volatle由java内存模型提出概念,由jvm去实现,写不能被缓存, 读的时候有些指令不能被重排。Volatile的语义并不是禁止所有的指令重排,而是cannot be cached.
线程可见性问题的分析和解决办法