
JVM总结-05_JMM内存模型和并发线程
JMM内存模型

java内存模型主要是定义程序中各种变量的访问规则,即虚拟机从内存中取出变量(不包括局部变量和方法入参),和把变量存储到内存中这样的底层细节。
主内存是线程共享,工作内存线程独有,每个工作内存读取变量都是读取主内存变量值的副本,每个变量在修改后都应同步到主存。
内存间的交互操作
各个操作都是原子的
-
lock 锁定:把主内存的变量标识为线程独占的状态
-
unlock 解锁:把主内存的变量从线程独占的状态释放,线程共享
-
read 读取:把主内存的变量值传给工作内存,便于后边的load动作
-
load 载入:把read到的变量放到工作内存的变量副本中
-
use 使用:把工作内存的变量值传递给执行引擎,虚拟机使用变量的字节码指令时执行
-
assign 赋值:把执行引擎的值赋值给工作内存变量,虚拟机赋值变量的字节码指令时执行
-
store 存储:把工作内存的变量传送到主内存中
-
write 写入:把store操作的变量写入到主内存的变量中
每条指令两两分组,成对出现,且严格按顺序执行(可以交替执行,比如read2次,再load两次)
volatile
jvm提供的最轻量级的同步机制
作用:
-
保证变量对所有线程的可见性:(其修改的值立马能够被其他线程看见)立即可见
普通变量的值修改后都需要由工作内存传输到主内存,再从主内存获取才能得到,此时才对其他线程可见
-
去除指令重排优化,强制顺序执行(单例模式双重检查),通过读写前加内存屏障
避免异步获取值时由于指令重排的原因导致获取到一个还没被初始化的对象
尽管volatile关键字保证其变量修改其他线程立即可见,但他也并不是线程安全的,
这是他保证立即可见的原理是因为他在工作内存每次使用该变量的时候都会对值先刷新,执行引擎看不到不一致的情况,所以看起来是一致的,
但是在java中的运算操作并不是原子的,如果多个线程对一个全局变量同时进行运算操作,还是会出现并发问题。
在满足以下场景中建议使用volatile
-
运算结果不依赖变量的当前值,或者能够确保只有单一线程来修改该变量值
-
变量不需要其他的状态变量来共同参与不变约束
指令重排
指令重排序是机器级别的优化,变量实例化操作
-
分配内存空间
-
初始化对象
-
对象引用指向内存空间地址
指令重排为了加快效率,有时2和3是乱序执行,导致多线程环境下对象实际上没有被初始化,但是引用地址已经分配好了,导致你获取了个空对象。
内存屏障
在指令序列中插入内存屏障来禁止指令重排
long和double这种64位数据实际上是分为两个32位操作,所以并不具有原子性,在32位系统可能会读取到一半的值。但是现代的64位系统不会出现这两个类型的原子性问题,
jdk9中加了-XX:AlwaysAutomicAccesses,保证原子性,现代的cpu中都带有专门的浮点数处理器,所以不用刻意给long和double加volatile
并发的三大要素
-
可见性
-
原子性
-
有序性
可见性
定义:一个变量的修改在其他线程中是立即可见
原因:CPU缓存(基于jmm可得知,线程获取变量需要从主存现同步到工作线程,期间的同步延迟就是造成可见性问题的根源)
实现:
-
volatile
-
synchronized
-
final
原子性
定义:基本数据类型的访问,读写操作具备原子性
原因:分时复用,(cpu会对线程进行切换执行)
实现:
-
java内存模型直接保证原子性变量操作:read,load,use,assign,store,write
-
synchronized关键字 -- monitorenter monitorexit
有序性
定义:java天然的有序性:在单线程环境下天然有序,在多线程环境下则是无序的
原因:重排序
线程内看似串行:with-in-thread-as-if-serial semantics
多线程无序:指令重排序、工作线程和主内存的同步延迟现象。
实现:
- synchronized
先行发生原则:happens-before
判断线程之间的偏序问题,判断数据是否存在竞争,线程是否安全
保障规则:
-
程序次序:一个线程内,按照控制流顺序,写在前面的先执行
-
管程锁定:同一个锁,一个unlock先发生在下一个lock前
-
volatile:对于一个volatile的变量,写操作先发生在后续的读操作前
-
线程启动:Thread对象的start()方法最先发生
-
线程终止:线程的所有操作都发生在线程终止前,Thread.join()是否结束,Thread.isAlive()判断线程是否已经终止
-
线程中断:interrupt()方法先发生与检测到中断事件前,Thread::interrupted()方法检测线程是否有中断事件发生
-
对象终结:一个对象的初始化完成发生在它finalize()方法前
-
传递性:如果A先发生于B,B先发生于C,那么A先发生于C
线程安全与锁优化
定义:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
线程安全类别
-
不可变
-
绝对线程安全
-
相对线程安全
-
线程兼容
-
线程对立
不可变
-
final修饰的基本数据类型一定是线程安全的
-
对于引用类型,需要定义者自己保证不可变,例如String,类中并没有提供修改的方法,也就保证的不可变。也就线程安全
-
还有枚举,包装类,(AutomicInteger,AutomicDouble除外)
绝对线程安全
-
不管运行时环境如何,调用者都不需要任何额外的同步措施
-
java大多数线程安全的类,都不是绝对线程安全
相对线程安全
-
通常意义上讲的线程安全,保证对该对象的单次操作是线程安全的,对于特定顺序,需要调用者同步。
-
Vector,HashTable,Collections的synchronizedCollection()方法包装的集合等
线程兼容
-
指对象本身不是线程安全的,但是通过同步可以并发环境下安全使用
-
javaAPI下大部分类是线程兼容的,如ArrayList和HashMap等
线程对立
- 不管调用端如何同步,都无法在多线程环境中并发使用代码,
线程安全的实现方法
-
互斥同步
-
非阻塞同步
-
无同步方案
阻塞同步(互斥同步)(悲观锁)
-
synchronized
经过编译后会在同步块的前后形成monitorenter和monitorexit,都需要一个reference对象来指明要锁定和解锁的对象,如果没指定,则以sync修饰的方法类型来确定加锁解锁对象类型。
执行monitorenter时,尝试获取锁,如果没指定对象,或者已经持有对象了,给锁计数器+1,执行到monitorexit时,计数器-1,为0时失败,若获取锁对象失败,阻塞等待。直到锁释放为止。
更多关于synchronized的详解看juc篇章,这里只做初步引入
-
同一个线程是可重入的
-
无法提前环形或解锁操作
-
-
Reentrant Lock
juc下的包,基于Lock接口,可重入的互斥锁
手动调用lock和unlock方法来实现加锁和解锁
-
等待可中断,长时间获取不到锁时可以中断放弃锁
-
可实现公平锁,多个线程等待同一个锁时,必需按时间顺序等待排队获取锁
-
锁可以绑定多个条件,一个Reentrant Lock对象可以绑定多个Condition对象
-
synchronized和Reentrant Lock性能:
ReentrantLock性能要稍微稳定一点,但是synchronized胜在语义简单,在简单场景可以使用。
非阻塞同步(乐观锁)
阻塞同步的问题是它是悲观的,遇到问题先加锁再说,其他的线程就被阻塞住了,所以在阻塞和唤醒的时候性能上比较拉胯
-
CAS
乐观锁的实现需要保证:操作,冲突检测,这两个行为至少是原子性的,cas是compareandswap(),是一个native方法,调用的是硬件的指令,所以是原子性的,三个操作数
-
V:变量内存地址
-
A:旧值
-
B:新值
CAS:V符合A时,把V更新成B,然后返回A
入一个参,这个参数等于旧值的时候,替换成另一个值,然后返回旧的值。
-
-
Automic Integer
juc下的AutomicInteger,原子类,其中的CompareAndSet()和getAndIncrement()都使用了Unsafe类的CAS操作。
-
ABA
ABA指的是如果一个值读取的时候是A值,替换成了B值,后来又改成了A,则CAS认为它没有变化。
juc包下提供带标记的原子引用类AtomicStampedReference 来控制变量值的版本号解决ABA问题,大部分情况下ABA问题不影响并发正确性。
基础类型则用 AtomicMarkableReference / 版本号自增
无同步方案
若一个线程本来就不涉及共享数据,就无需线程同步的方案。
-
栈封闭
栈帧内的局部变量是线程私有的所以不会出现线程安全问题,也就不需要同步方案。
-
线程本地存储**(Thread Local Storage)**
Thread Local 线程私有,互不影响。
ThreadLocal底层ThreadLocalMap有可能在一些场景(线程池)下存在内存泄漏的风险,所以每次不需要使用了的时候要注意调用remove()方法。
- 线程池回收线程,但是没有回收ThreadLocal,
-
可重入代码
可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
不依赖全局变量、存储在堆上的数据和公用的系统资源。
(我感觉就是一个方法里边儿没有全局变量和对象,或者一些公用的资源,巴拉巴拉的)