最近看了一些关于java并发编程的知识,小厂工作用到的不多,但是知识储备还是得有呀,毕竟面试会用到。下面是总结的知识点、一下全部针对(单机、多CPU、多核)的硬件架构
知识点储备篇(餐前甜点)
1、关于当前计算机硬件(多CPU、多核)
现代一台计算机的机构通常又多个CPU组成、每个CPU又有一定的核数。我们的java运行的线程都是在CPU中的寄存器执行、当一个线程执行的时候首先会从ARM中将变量copy一份放置到CPU cache(CPU缓存区)中、再由集群器执行,执行完毕之后再写回到CPU cache再由cache写回到内存
问1:为什么会有这么多CPU cache呢?
因为CPU的运行速度和执行速度远高于内存,为了避免每次CPU运行作从内存中取值,从而设置L1、L2、L3三个缓存区,每次CPU执行从缓存中取值。三个内存区的运行速度都不同、L1>L2>L3也就是说,寄存器每次运行的时候会从L1中取值、L2则为L1的缓存区、以此类推
2、缓存一致性协议(MESI)
在上述环境中,当多个处理器的运算任务都涉及同一 块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步 回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都 遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等
MESI : M 修改 (Modified)、E 独享、互斥 (Exclusive)、S 共享 (Shared)、I 无效 (Invalid)
问2:这里的MESI协议是如何工作的呢?
举一个例子、当一个线程T1和另一个线程T2同时要修改主内存的变量i时、T1竞争到CPU1的使用权,copy一份变量i到自己的缓存中当前i的状态是E(独享、互斥)。另一个线程T2也竞争到CPU2开始操作变量i,copy一份到自己的缓存中、此时CPU1会嗅探到CPU2也在使用这个变量于是乎将状态改为S(共享 )、CPU2将copy的i状态也变为S(共享 )。两个CPU执行各自的操作,假如CPU1先执行结束,会将状态置为M(修改)而CPU2通过嗅探到CPU1状态的更变将自己的缓存区的状态改为I(无效),并且重新加载主内存中变量i的值进行运算。
除了MESI协议之外还有一种方式可以处理缓存一致性的问题,那就是加锁。Bus Locking(总线锁)。当一个CPU对其缓存中的数据进行操作的时候,往总线中发送一个Lock信号。这个时候,所有CPU收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的CPU就去内存中获取最新数据更新。
3、什么是线程上下文切换
JMM和volatile(正餐)
1、什么是JMM
官方语言 : JMM (Java Memory Model)是Java内存模型,JMM定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节。 为什么要设计JMM 屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
大白话 : JMM是一个抽象概念、计算机的操作最终都是在硬件上的、JMM的存在是为了屏蔽掉我们各种硬件或者操作系统之间的差异,让我们的代码达到在任何系统上都一致访问内存的效果(和上面的差不多,哈哈哈哈哈哈)
JMM中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
此时就应该了解JMM是什么了,并且知道JMM和JVM的区别
2、为什么需要JMM
3、JMM八种操作
4、并发编程的可见性,原子性与有序性问题(全是理论知识,了解即可)
原子性: 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
5、JMM如何解决并发编程的三大特性
所有的指令重排都必须要as-if-serial语义 : 即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序
那么回来volatile如何能保证有序性(避免指令重排)这里还得说一个知识点叫内存屏障(内存栅栏)
这是工作中一般会用到的单例模式,但是如Demo上展示的一样,这种没有使用volatile关键字的写法是否会有问题,如果在单线程下工作不会出现问题,但是在多线程下会出现线程安全的问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
指令看上步骤2和步骤3没有依赖关系、这种数据的重排是可以被允许的、当第二个线程进来时可能已经指向了内存地址,但是还没有初始化对象,所以也会有线程安全问题、一般会使用volatile 禁止重排。
拓展内容(小朋友你是否有很多问号)
1、除了volatile之外还有什么办法添加内存屏障吗?
我们知道了volatile可以添加内存屏障保证有序性,除此之外还有什么办法呢?
在Java中的sun.misc包下提供了一个类叫Unsafe的类,这个类无法new出来、一般使用通过反射来执行(有兴趣的小伙伴可以试试)这个类如果玩不明白的话不建议玩、Unsafe不属于jvm操控,但是会直接操控内存,一但玩坏了会引发很多内存问题(谨慎使用)。关于内存屏障的方法如下
loadFence() 在该方法之前的所有读操作,一定在load屏障之前执行完成。
storeFence() 在该方法之前的所有写操作,一定在store屏障之前执行完成
fullFence() 在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。
2、你晓不晓得总线风暴?
什么是总线风暴?
我们看过MESI协议之后就了解到,协议的工作就是当线程获取主内存的变量时,需要不停的去嗅探其他线程是否也是用此变量,而访问主内存都需要通过BUS总线、因此BUS总线的带宽达到峰值。这就是总线风暴。JMM的CAS操作也会引起总线风暴、可以自己了解下
如何解决总线风暴呢?
我们知道了总线风暴的问题所在也就自然找到了问题的所在,解决办法就是减少volatile关键字的使用和CAS行为,可以使用synchronize,lock等来代替。
这篇知识点就完结了,接下来写啥呢~~~