Understand JVM Part IV
文章目录
内存模型与并发
1 内存模型
1.1 硬件层面的缓存一致性
- CPU核心、高速缓存与主内存之间通过缓存一致性协议保持数据的版本问题
1.2 Java内存模型
1.2.1 Java内存模型概述
- 每个线程拥有自己的工作内存,不同线程间的工作内存不可见,线程所有操作都操作工作内存
- 工作内存与主内存之间的数据同步通过定义的一些原子操作实现(lock、unlock、read、load、use、assign(赋值)、store、write)(1.5之后不再使用这种方式描述内存模型,使用先行发生原则)
1.2.2 volatile变量
- volatile只保证可见性,并非线程安全,多线程仍然会出现不安全的
- 运算结果不依赖当前volatile变量或不需要与其他变量共同参与不变约束时,推荐使用volatile
- volatile禁止指令重排序,线程内表现为串行
1.2.3 long与double
- Java内存模型不保证64位数据类型的load、store、read、write 4个操作的原子性,long与double的非原子性协定,但现在的虚拟机已经实现原子性
1.2.4 原子性、可见性与有序性
- 原子性:字节码monitorenter与monitorexit实现(管程)
- 可见性:新值同步主存,读取从主存刷新方式实现,另外,volatile保证新值立即同步主存,读取时立即从内存刷新,synchronized块中,对一个变量执行unlock之前必须刷回主存,final保证一单初始化完成,其他线程立即可见
- 有序性:线程内观察,所有操作有序,观察其他线程,所有操作无序(线程内表现为串行语义;指令重排序与工作内存与主存的差异)
1.2.5 先行发生原则
- Java内存模型中存在天然的先后操作顺序,无需同步措施,如果两个操作不属于这些规则,则不保证顺序性
- 先行发生原则:
- 程序次序: 最终按照代码顺序执行
- 管程锁定: unlock操作先于同一个锁的后一个lock操作(时间的先后)
- volatile变量:对一个volatile变量的写先于后面的读(时间的先后)
- 线程启动: Thread的start先于线程的每一个动作
- 线程终止: 线程中的左右操作都先于对该线程的终止检测
- 线程中断: 对线程interrupt()方法的调用先于被中断线程的代码检测到中断事件的发生
- 对象终结: 一个对象的初始化完成先于finalize()方法
- 传递性: A先于B,B先于C,A先于C
- 一个操作
时间上的先发生
不代表该操作先行发生
1.3 Java与线程
1.3.1 线程的实现
- 使用内核线程实现:程序使用轻量级进程LWP(内核线程的高级接口)进行线程映射,每个轻量级进程对应一个内核线程,这种1:1的关系成为一对一的线程模型,线程的所有操作需要进行系统调用,需要用户态和内核态进行切换,轻量级进程需要消耗一定的内核资源,一个系统支持的轻量级进程数量是有限的
- 使用用户线程实现:系统内核不能感知用户线程的存在,其建立、同步、销毁和调度全在用户态完成,不需要内核的帮助,这种进程与用户线程的1:N的关系成为一对多的线程模型,难以实现,逐步放弃
- 使用用户线程+LWP:以上两种结合推理
- Java实现:Windows与Linux使用一对一线程模型实现,Windows与Linux提供的线程模型就是一对一的
1.3.2 线程调度
- 协同式调度(协程):线程主动通知系统切换线程,如Lua的协程,缺点执行时间不可控制,某个线程可能一直不通知
- 抢占式调度:每个线程由系统来分配执行时间,Java采用,Java提供了10中线程的优先级来代表次序,但是Java的优先级不一定靠谱,有的OS提供的线程优先级不一致,如windows提供7种,无法与之一一对应,此外优先级可能被OS的各种优化与功能改变
1.3.3 状态转换
2 线程安全与锁优化
2.1 线程安全
- 多线程访问一个对象,不用考虑线程的调度与交替运行,也不需要进行额外的同步,调用这个对象的行为产生正确的结果
2.1.1 Java的线程安全
- Java的安全性:
- 不可变
- 绝对线程安全
- 相对线程安全(单独的操作是线程安全的,但是对于特定的顺序连续调用,需要额外的同步手段,如Vector的读取与删除同时进行)
- 线程兼容(对象不是线程安全,可以使用同步手段达到线程安全)
- 线程对立(无法在多线程环境运行,Threadsuspend()、Thread.resume()可能引发死锁)
2.1.2 Java的线程安全的实现方法
互斥同步:
synchronized
关键字实现,字节码指令对应monitorenter
与monitorexit
,这两个字节码需要一个reference
类型参数,如果Java代码未指定,则为对象实例或Class对象,进入该指令时,首先要获取对象的锁,如果当前线程已经获取则将计数器+1,线程可重入,不会锁死自己,由于该语句会导致线程状态切换,线程需要进入核心态,消耗较大- 也可以使用
ReentrantLock
,性能更好, 但1.6之后synchronized
关键字的吞吐量已经大幅提高,推荐使用原生synchronized
关键字(1.8 ConcurrentHashMap源码已经使用synchronized
来实现,Doug lea已经认可了synchronized
的性能)
- 也可以使用
非阻塞同步:CAS指令,需要硬件指令集支持(比较和更新需要原子操作),需要三个参数,内存位置,预期值,新值,Java中使用Unsafe类提供该操作
- CAS指令会有ABA问题,可以使用
AtomicStampedReference
来控制变量的版本,更推荐使用互斥同步解决,因为一般ABA不影响并发的正确性。 - 该类只支持
Bootstrap ClassLoader
加载,用户程序默认无法使用,只可以通过反射:1234567891011121314151617181920212223242526272829303132333435363738/*** 一个简单的示例* Date: 2017/6/13* Time: 17:04*/public class CasSample {public volatile long casVar;private static long CASVAR_OFFSET;static Unsafe unsafe;static {try {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);unsafe = (Unsafe) field.get(null);Class<?> k = CasSample.class;//取偏移量 实例变量使用objectFieldOffset,类静态变量使用staticFieldOffsetCASVAR_OFFSET = unsafe.objectFieldOffset(k.getField("casVar"));} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {CasSample casSample = new CasSample();casSample.casVar = 1;casSample.CAS_Swap();}private void CAS_Swap() {unsafe.compareAndSwapInt(this, CASVAR_OFFSET, 1, 2);System.out.println("cas" + casVar);}}
- CAS指令会有ABA问题,可以使用
2.2 锁优化
- JVM层面的优化
2.2.1 自旋锁与自适应锁
- 自旋锁:互斥同步线程阻塞不先不进入内核态挂起,而是让线程执行一个自旋,期待循环之后别的线程就可以释放锁了,自旋占用CPU时间,不可无限制自旋,自旋次数默认10
- 自适应锁:自旋的时间不固定了,由前一次在同一个锁上的自旋时间以及锁拥有着的状态决定,比如上次自旋成功获得了锁A,当前持有锁的线程正在运行,虚拟机判断本次自旋获取到锁的可能性很大,进而自旋100次
2.2.2 锁消除
- 自动判断当前代码块内是否会出现共享数据竞争,如果没有竞争,则将该段的同步措施去除。
2.2.3 锁粗化
- 锁的使用通常要求锁的范围越小越好,但是若一系列的操作对同一个对象频繁的加锁解锁,比如出现在循环体内,那么这种加锁导致不必要的性能损耗,虚拟机探测到后会将同步措施放在整个操作序列的外部。
2.2.4 轻量级锁
假设是对于绝大部分锁,整个同步周期内不存在竞争,根据对象头中的信息来表示对象的锁信息,参考
如果对象没有被锁定,即锁标志位为
01
,则JVM在栈帧中创建一个锁记录的空间(Lock Record),存储当前对象的Mark Word备份,CAS更新对象的Mark Word为Lock Record的指针,若成功,则表示该线程获得了锁,且锁标志位变为00
,此时对象处于轻量级锁定状态、如果上述CAS失败,JVM检查该对象的Mark Word是否指向当前线程的栈帧,如果是则表示该线程已经获得锁,直接进入同步块执行,若失败,则锁膨胀为重量级锁,锁标志位变为
10
,Mark Word中存储重量级锁的指针(互斥量),后续等待线程进入阻塞状态解锁依然使用CAS,若Mark Word依然指向线程的锁记录,则CAS替换回来那份拷贝,成功则完毕,失败则表示锁定期间有其他线程尝试获取改锁(此时Mark Word指向的是互斥量),释放锁同时唤醒被挂起的线程
2.2.5 偏向锁
- 据统计,在实际情况下,大部分的锁对象永远只被一个线程占用,轻量级锁在每次monitorenter和monitorexit的时候(非重入),都会进行一次cas操作,为了进一步减少cas操作,偏向锁(biased locking)诞生了。是否启用偏向锁由Mark Word中的一位表示,位于锁标记位前,可偏向为
1
- 锁对象第一次被线程获取,CAS将线程ID设置在Mark Word中,成功则该线程进入同步块时,不再进行任何同步操作
- 另外一个线程获取锁时:
- 看一眼持有锁的线程是否还活着,如果已经死了,那将对象设置为无锁状态就可以了
- 如果这个线程还活着,那需要遍历持有这个锁的线程的栈帧中的所有lock record,如果所有lock record都不指向锁对象,那么这个线程实际上不持有这个对象锁。同1,将对象设置为无锁状态即可
- 如果这个线程正在占用这个锁对象,那么需要修改线程中的锁记录与对象头,将它们都修改为轻量级锁状态,然后正常走轻量级锁的流程即可,是否偏向锁标记位为
0