脚本宝典收集整理的这篇文章主要介绍了Java并发系列「2」-- 并发的特性;,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
@TOC# Java并发系列 记录在程序走的每一步___auth:huf
package com.huf;
public class ThreadTest2 {
private volatile static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10 ; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
count++;
}
}).start();
}
Thread.sleep(3000);
System.out.println(count);
}
}
我们看到 这段代码 就是开启了十条线程; 每条线程 我们都对count进行了+1 然后再volatile 进行了修饰。 我们不妨大胆的猜一下结果 是什么?
结果是8949; 预期结果 应该是 10000; 为什么会出现这样的事情?难道没执行完吗?画一张图来了解 CPU内部结构;
假设我们程序 在Registers 没有找到需要的参数 就会继续去 CPU 缓存中进行命中目标 ; 一般来说: 越低等级速度越快(被访问的速度);越高等级容量越高(CPU缓存容量);
时间局部性,意思是 我们在使用一个参数 ,那么它 近期很有可能再次被访问
空间局部性,我们读取内存是一片一片读取的。 也就是说 一个位置被引用 那么它附件的位置也有可能被引用。 (例:我们Mysql 每一次取数据 都会抓取4KB)
我们可以通过一张图 来清晰明了的知道他们之间的关系
下面我们来开始升级这张图; 我们在这张图上做文章;我们的主内存中 有一个变量 a = 3
我们《线程一》 执行 a+ 6的操作
我们《线程二》 执行 a+ 8的操作
我们现在可以预测:
我们《线程一》 去加载 a 然后 load 进CPU :
开始进行运算 a 此时等于 3 那么 core 1的结果 是: 9
我们《线程二》 也去加载 a 然后load 进入CPU :
开始运算 a 此时也等于3 那么 core 2的结果 是: 11
我们的主内存中 的结果 有可能 == 9 也有可能 ==11 结果是不确定的
确保一致性的两种最常见的机制是窥探机制(snooping )和基于目录的机制(directory- based),这两种机制各有优缺点。如果有足够的带宽可用,基于协议的窥探往往会更快,因为 所有事务都是所有处理器看到的请求/响应。其缺点是窥探是不可扩展的。每个请求都必须广播 到系统中的所有节点,这意味着随着系统变大,(逻辑或物理)总线的大小及其提供的带宽也必须 增加。另一方面,目录往往有更长的延迟(3跳 请求/转发/响应),但使用更少的带宽,因为消息 是点对点的,而不是广播的。由于这个原因,许多较大的系统(>64处理器)使用这种类型的缓存 一致性。
当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有 该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。 窥探机制有两种协议: Write-invalidate 写失效; 也就是 在我们 x在我们的core被运算成为 9的时候 直接告诉其他副本 失效 常用协议有 MSI MOSI MESI MOESI等等
Write-update :写 更新 常用协议有: Dragon和firefly 但是不是经常能看到
由于总线锁定阻止了被阻塞处理器和所有内存之间的通信,而输出LOCK#信号的CPU可能 只需要锁住特定的一块内存区域,因此总线锁定开销较大。 缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那 么当它执行锁操作回写到内存时,处理器不会在总线上声言LOCK#信号(总线锁定信号),而 是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制 会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的 数据时,会使缓存行无效。
缓存锁定不能使用的特殊情况:
MESI 不难; 他代表的是数据在CPU高速缓存中的一种状态;(我个人是这样记忆的)
MESI协议是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用 协议。也称作伊利诺伊协议 (Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明 的)。与写通过(write through)缓存相比,回写缓冲能节约大量带宽。总是 有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss) 且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主 存的事务数量。这极大改善了性能。
M:-Modified 修改:
当数据从 CPU缓存中 进入CPU 寄存器 并且在计算因子ALU计算完成
计算后 回写到CPU高速缓存中 这时候 数据状态就是M
这时候 会通过Lock前缀指令立即刷新到缓存中
并且通知其他 副本缓存立即失效(I)
这时候数据只有它自己独一份 是不失效的 此时状态转变为E
E:-Exclusive 独占:
数据是E的时候 代表数据它是独占的。
如果此时进来第二条线程。这是访问该数据
那么就会产生一摸一样的副本进入另外一个core
S:-Shared 共享
当独享的时候 这时候进来了第二条线程
这时候产生了副本 同时后也改变这两个数据的状态
这时候状态就是 S
I:-Invalid 失效
当数据被CPU 计算因子 ALU计算出来 并且回写到内存中的时候。
这时候 其他CPU副本的数据 状态就会变成 I
这样去理解总线锁定。 我们黄色的区域 一旦锁定了内存中的 某个值的时候 我们core2 是没有办法读取内存的。 这样我们就变成了串行化执行了。
比较专业一点的解答: 总线锁定就是使用处理器提供的一个 LOCK#信号,当其中一个处理器在总线上输出此信号 时,其它处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
首先我们来 看一下什么叫做伪共享;
我们的数据再内存中 存储 叫做 缓存行 其中缓存行的大小一般为64byte
我们再内存中开辟一个空间 并且 用两个线程来对该空间的数据进行操作;
package com.huf;
另外一个class
class Test{ volatile long a; volatile long b; }
public class ThreadTest3 {
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
Test test = new Test();
Thread thread = new Thread(()->{
for (int i = 0; i < 100000000; i++) {
test.a++;
}
});
Thread thread2 =new Thread(()->{
for (int i = 0; i < 100000000; i++) {
test.b++;
}
});
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(test.a);
System.out.println(test.b);
System.out.println(System.currentTimeMillis()-startTime);
}
}
从JDK 5 开始,JMM使用happens-before的概念来阐述多线程之间的内存可见性。在JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens- before关系。 happens-before和JMM;
happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依 靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。下面我们就一个 简单的例子稍微了解下happens-before :
i = 1; //线程A执行
j = i ; //线程B执行
j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么可以 确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成 立。这就是happens-before原则的威力。
这里再说一遍happens-before的概念:如果两个操作不存在上述 任 一 一 个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排 序。如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。
以上是脚本宝典为你收集整理的Java并发系列「2」-- 并发的特性;全部内容,希望文章能够帮你解决Java并发系列「2」-- 并发的特性;所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。