内存模型
JMM模型
JMM实际上就是CPU特有的一级二级缓存与共享内存之间的关系。
每个cpu有自己的缓存,上级缓存从下级缓存中选取数据,所有缓存都是通过三级缓存和内存做交换,而三级缓存是所有cpu共有的。
重排序
重排序场景
1 编译器重排,为了避免重复计算或读写,完成一些重排,并给指令添加内存屏障,防止指令重排时出现不符合语义的重排
2 指令重排,为了保证ILP流水线的并行,重排指令。和编译器重排的区别在于,上一级把整条指令作为是否重排的判断,而这一级把指令拆分成若干阶段,以指令的阶段作为是否重排的判断标准。
3 读写缓存,缓存和实际内存必然导致一致性问题产生一种和重排相似的效果
重排序问题避免
以下两种协议是JVM用于限制指令重排,防止出错的。
as if serial 是一些用来保证单线程顺利执行的规则
happens-before是一些用来保证单线程顺利执行的规则,如果调换之后的预期结果不变,happens-before可以不被遵守
volatile
语义
volatile写之前所有写对后续读可见,volatile读的是数据最新版本
原理
简介
要做到保证volatile语义,就需要保证两件事,语句重排不会影响volatile语义和各cpu并行执行volatile命令时不会相互重叠
语句重排通过屏障保护语义
volatile写之前通过storestore屏障,这是因为volatile写之后所有缓存会写回内存,为了保证此语义,所有之前写操作不能被延后
volatile写之后通过storeload屏障,保证一切读指令不能到volatile写之前,保证了读写的可见性
volatile读之前不需要屏障,因为volatile读之后storeload屏障保证了读指令不可能在volatile写之前
volatile读之后需要loadstore和loadload屏障,这个能起到的作用是防止指令后移,只能前移(并且不可以前移到volatile读/写前)
实际上编译时可以省下不少屏障
cpu并行处理命令时
volatile写通过总线锁或缓存锁保证同步
总线锁可以直接锁总线,直接改内存,废弃其它缓存。
而缓存锁只锁自己对应的缓存行,并不阻止其他cpu缓存写回内存,所以需要配合缓存一致性协议来避免写回冲突。
MESI
一种常见的缓存一致性协议,也是volatile所采用的
M:Modified,自己修改了,没来得及写回内存
E:Exclusive,自己修改了,还写回内存了
S:Shared,自己保存着和其他缓存,主内存相同的数据
I:Invalid,自己保存的缓存数据已经无效了
一开始全部cpu共享内存中一个数据,属于s状态,如果有一个cpu提出volatile写,就会变成m,如果是多个cpu同时提出volatile写,就会引起总线裁决,只有一个会变成m状态,其余就会变成i状态。
比如在atomic包中,原子操作Integer完成一次累加,往往要先取值完成一次volatile读,然后再提出累加,如果失败,就会重复这个过程。
这是因为volatile写竞争失败的cpu想要继续完成写操作,必须保证自己不是i状态,要重新读取内存数据,更改自己状态为s,然后才可以继续发出写信号。
如果volatile写竞争成功,那么自己会变成m状态,如果写回成功,那自己会变为e状态。在m状态变成e状态之后,其他cpu才可以正常读取内存数据。
如果在volatile读时发现自己是i状态,就会去获取内存数据获取新的缓存。
缓存行锁定时的小trick
由于一行往往是64字节,所以有时候对于并发读写非常频繁的地方,可以将缓存行填充到64字节。
如图,由于queue对首尾节点访问非常频繁,所以把首尾节点都填充到64字节。
volatile实战
如图,内存分配实际上有三步组成,分配空间,初始化对象,引用指向,而后才允许其他线程读取,如果顺序变为分配空间,引用指向,然后其它线程读取,发现部位null,就会返回空对象。
坏处
频繁使用volatile会导致总线上很多缓存行状态更改信息,而占用了真正传递信息的资源。
而如果在高并发时候,由于CAS的存在,这个现象会更加明显。
CAS
unsafe
unsafe是一个危险的类,因为它通过地址直接对数据进行操作,里面是一些完成get,set和CAS的本地方法,一般建议使用atomic包下的类,它们对unsafe进行了一层封装
使用
分为数值原子操作,引用原子操作,数组原子操作,和属性原子操作
操作简单,没什么好说的
并发工具类
CyclicBarrier
CyclicBarrier初始化时会放入等待数和Runnable。
如果线程到达了await(),就会减少等待数,如果没到0,就会阻塞,如果到0了,就会执行Runnable,然后放行所有之前阻塞线程。
public class CyclicBarrierTest implements Runnable{
/**
* 使用 CyclicBarrier
*/
CyclicBarrier cb = new CyclicBarrier(2,this);
int[] array = new int[2];
public void calcute(){
Thread A = new Thread(() -> {
//计算 3*5
array[0] = 3*5;
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread B = new Thread(() -> {
//计算 10+2
array[1] = 10+2;
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
A.start();
B.start();
}
@Override
public void run() {
System.out.println(array[0] + array[1]);
}
public static void main(String[] args) {
CyclicBarrierTest2 cyclicBarrierTest2 = new CyclicBarrierTest2();
cyclicBarrierTest2.calcute();
}
// 我们的cycbarrier 能够支持一个runnable的action去做后续的数据的操作。能够适用于更加复杂的
// 场景。
}
Semaphore
用来限制资源数,如下,100个线程只有10个可以获取资源继续访问
public class SemaphoreTest {
static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for(int i = 0 ; i < 100; i++) {
Thread a = new Thread(() -> {
try {
s.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("链接db,保存数据。");
s.release();
});
a.start();
}
}
}
声明
参考了河北王校长的视频
来源是本人语雀笔记(语雀分享竟然收费了!失踪人口回归)