最近看了两篇博文:
主题:java线程安全总结
主题:线程安全总结(二)
看完之后对自己触动很大,和自己以前的理解有很大的矛盾.当时发站内信给作者的疑惑内容如下:
写道
看完之后,有2个疑问:
⒈java内存模型,没有说内存模型的具体结构,如:heap,java stack,method area等.
2.关于"那么,何谓可见性? 多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。当线程操作某个对象时,执行顺序如下:
(1) 从主存复制变量到当前工作内存 (read and load)
(2) 执行代码,改变共享变量值 (use and assign)
(3) 用工作内存数据刷新主存相关内容 (store and write) JVM规范定义了线程对主存的操作指令:read,load,use,assign,store,write。当一个共享便变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。"
疑问:为什么要有很多的副本呢?然后再刷新到主存呢?这样做不是效率很低?
java中的方法是值传递,参数是对象的话,只是对象指针的一个副本,如:
public void fun(Object obj){
// 改变obj的属性
obj.property = "abc";
// 改变obj指针的值,对方法外面的obj无影响
obj = new Object();
obj.property = "def";
}
既然每个线程中java stack传递的都是指针(指向heap内的对象),为什么还要复制一个heap内对象的副本呢?
后来看到第二篇的时候
写道
看完之后明白了,我以前的理解只是基于内存中的模型,而你说的read,load,use,assign,store,write针对的是对CPU,寄存器,内存等统一抽象的工作内存的操作
谢谢分享!让我对线程安全的理解更深入一步
今天,特意的查询了这方面的内容,博客地址:http://kenwublog.com/illustrate-memory-reordering-in-cpu
写道
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。
重排序的背景
我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多。当CPU的计算速度远远超过访问cache时,会产生cache wait,过多的cache wait就会造成性能瓶颈。
针对这种情况,多数架构(包括X86)采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个 slots (逻辑存储单元,又名 Memory Bank 或 Cache Bank),CPU可以自行选择在多个 idle bank 中进行存取。这种 SMP 的设计,显著提高了CPU的并行处理能力,也回避了cache访问瓶颈。
Memory Bank的划分
一般 Memory bank 是按cache address来划分的。比如 偶数adress 0×12345000 分到 bank 0, 奇数address 0×12345100 分到 bank1。
重排序的种类
编译期重排。编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。
运行期重排,CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。
实例讲解指令重排序原理
为了方便理解,我们先来看一张CPU内部结构图。
从图中可以看到,这是一台配备双CPU的计算机,cache 按地址被分成了两块 cache banks,分别是 cache bank0 和 cache bank1 。
最后引用博客作者的一句话:
写道
先抛开java虚拟机不谈,我们都知道,现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当个多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特性:原子性,有序性,可见性
而实现这个的就是编译期重排和运行时重排序