JVM并发同步机制
需要参考的准备数据:
《深入理解JAVA虚拟机》
需要参考的知识点:
volatile原理
unsafe.compareandswap原理
多线程
amdahl定律(在Cpu个数很大的情况下,可并行解决的子问题越多阿姆达尔值越大)
final可见性原理
amdahl = (Ws + Wp)/(Ws + Wp/n)
知识的记录方式:
- 查看博客,把没有遇见过的或者觉得比较经典的博文段落摘录
- 自己的理解以条目的形式展示
- 知识误解标记
- 知识盲区标记
- JAVA内存模型这个知识点基本上每本书都会讲解,最好的方式将每本的书的这一章都读一下,然后摘录重要的知识点,通过反复和串联达到效果。
重要笔记:
线程安全的在《Java Concurrency In Practice》中定义:当多线程访问一个对象时,如果不考虑多线程在运行期的调度和交替执行的情况下,不采用任何额外的同步,或者借助其他方法协助操作,调用这个对象的行为都能得到正确的结果,那么这个对象就是线程安全的
-
定义分析:前提条件是调度正常的多线程并发执行的环境,约束是不能使用额外的同步机制以及协助方法,要求调用这个对象的行为能得到正确的行为结果。
-
在IBM developWorkers一篇论文中讲线程安全由强到低划分为5个等级:“不可变”、“绝对线程安全”、“相对线程安全”、“线程兼容”、“线程对立”
-
final修饰的对象在构造完成后的不可变(Immutable)可以保证最高级别的线程安全。但是在构造期间可能会发生this指针逃逸(对象还未被构造,而this被赋值出去了),所以final除了不可变性之外,还有一个特点就是编译之后会加入内存屏障,保证构建期间的内存可见效。应用场景:单例模式中的holder引用都会被final修饰。
-
根据线程安全的定义,“要求调用对象的行为都能得到正确的结果”,也就是说我们除了“访问不可变对象成员”这个行为之外,还可能会“并发访问对象同一个成员方法”以及“并发访问对象的多个方法”。能做到这一点的对象才能称作“绝对线程安全
-
并发访问同一个对象的,可以通过对象锁来实现。但“绝对线程安全”还要求“并发访问对象的多个方法”,这些方法中包含了读对象状态操作和写对象状态操作,那么就可能会出现脏读,从而造成错误结果。(线程安全约束要求不需要使用其他协助方法和同步,即不需要事务安全)。既然对对象的多个方法并发调用会出现脏读写,对象就无法满足“绝对线程安全”的定义了,即无法满足《Java Concurrency In Practice》中线程安全定义。除非对象的方法全是写操作,或者全是读操作(全是读操作,那么对象就是一个不可变对象了,不可变对象是线程安全的)。
-
java中@Immutable注解的类一定是绝对线程安全的(满足《Java Concurrency In Practice》中线程安全定义),而@ThreadSafe注解的大部分不是“绝对线程安全的”,即无法满足《Java Concurrency In Practice》中线程安全定义。
-
java中@ThreadSafe注解的对象包括了“绝对线程安全”与“相对线程安全”。其他普通对象都属于“线程兼容”,可以通过协助方法和同步手段达到线程安全的效果。而一些特殊的对象,无论采用什么协助方法和同步手段都无法实现多线程安全就被称之为“线程对立”,比如:“Thread对象的suspend(),resume()会死锁”,“System对象的setIn()setOut()runFinalizersOnExit()"。
-
悲观锁会产生阻塞,阻塞涉及到内核状态转移(线程状态改变)非常损害性能。因此才有了乐观锁概念,乐观锁需要操作系统底层提供同时具有“检测和操作”功能的原子指令,并且当乐观锁出现冲突时(数据脏读)需要采用“补偿机制”,比如“重试机制”。乐观锁机制也被称为“非阻塞同步”。
-
同步具有特点:阻塞影响性能(重量锁缺点),原生同步syntronized条件单一无限期阻塞(reentrantlock),自旋占用CPU(自适应自旋),粒度小的锁太多会导致加锁解锁频繁影响性能(粗化锁优化),发生竞争概率低(轻量锁和偏向锁)。
同步缺点 | 优化方案 |
---|---|
阻塞影响性能 | 改用自旋10次(1.6版本默认机制)或者采用无阻塞同步CAS |
原生同步syntronized条件单一无限期阻塞 | 改用reentrantlock |
自旋占用CPU | 改用自适应自旋锁 |
粒度小的锁太多会导致加锁解锁频繁影响性能 | 利用JIT即时编译器会粗化锁的范围 |
发生竞争概率低 | 采用单一线程无竞争采用偏向锁,多线程无竞争采用轻量锁,有竞争采用重量锁 |
- 以上分类比较凌乱,如果从程序猿角度去理解会更好。
从程序猿的角度,要保证程序在多线程环境下能够得到正确的结果。除了共享对象本身具备了“线程安全的特性”,程序猿还需要通过一个额外的同步手段和协助方法。
这句话的理论依据是“《Java Concurrency in Practice》中关于线程安全的定义。”。
"线程安全的定义": 在多线程访问对象时,如果不考虑多线程在运行期时调度状态替换和执行,且不采用额外的同步手段和协助方法,所有线程操作对象的行为都能得到正确的结果。
也就是说,从程序猿的角度来看,不考虑多线程运行期的正常调度和执行情况下,要保证多线程的所有对象访问行为都能得到正确的结果。而这保证的手段就是利用“额外的 同步手段和协助方法”
-
额外的协助手段有3种:
- 采用互斥同步手段(syntronized临界区,reentrantlock互斥量,semaphore信号量)
2.采用非阻塞同步(如使用原子操作类AtomicInteger等CAS方法)
3.编写可重入的纯代码或者使用ThreadLocal本地变量,或采用消息队列。
- 采用互斥同步手段(syntronized临界区,reentrantlock互斥量,semaphore信号量)
-
而问题比较多的同步是临界区,临界区JVM原生的同步机制。对这一块的优化理论性比较强,很值学习。临界区主要问题是“系统内核访问”和“重复加解锁”,所有的优化也是针对这两个问题进行的。
- 系统内核访问:
- 当出现竞争时所有线程都会发生阻塞和唤醒两个内核操作消耗性能,为了避免这种消耗,可以让线程进行自旋,当自旋到一定时间后再进行阻塞。
- 而盲目的自旋操作会导致CPU浪费,因此JVM1.6版本引入和自适应自旋锁(灵活的有限自旋),它会根据上一次的等待时间以及当前持有锁的线程状态来决定是否停止自旋。
- 阻塞和自旋、采用内核mutex互斥量都是重量级锁带有的特征,重量级会用在经常出现多线程竞争的地方。而大部分业务并不会为经常出现条件竞争,仅仅为了预防万一出现竞争才加锁。因此在默认情况下JVM会采用轻量级锁来应付临界区同步问题。
- 轻量级锁不需要引入复杂的ObjectMonitor对象以及系统内核的互斥量mutex函数(包括阻塞队列的唤醒机制,竞争阻塞这些封装在ObjectMonitor里面的东西都一概不需要),一旦出现竞争,轻量级锁将会不可逆的进化为重量级锁。
- 系统内核访问:
-
重复加解锁问题
- 出现这个问题的原因是锁的粒度设置太小,JVM会自动将这类锁优化成一个粗锁。如果锁的粒度太大会诱发长时间自旋,从而演变为阻塞。
- 为了避免频繁的加解锁,JVM引入了“偏向锁”机制。当临界区通常情况下只会被一个线程访问,那采用偏向锁。该单一线程只需在第一次使用的时候做一下声明。之后,进入退去临界区都不需要执行麻烦的加锁解锁操作。万一有2个以上线程访问(不管是否发生竞争),“偏向锁机制”就会被不可逆的进化为“轻量级锁”。进化的过程其实有些影响性能,因为偏向锁在升级为轻量级锁的时候会涉及停止和唤醒偏向锁持有线程等操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bGWcrRQn-1579504982285)(https://i.loli.net/2019/04/26/5cc2c8e747cac.png)]