本文主要是介绍【多线程】锁消除、锁粗化、偏向锁、自旋锁、自适应字段锁、轻量级锁、重量级锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- 锁
- 1. 锁的概念
- 2. 锁的状态
- 3. 锁的优化策略
- 1. 锁消除(编译器的操作)
- 1. 优点
- 2. 锁消除的原理(怎么实现的?逃逸分析)
- 1. 问:JVM如何在编译期消除锁的?
- 2. 题外话:什么是逃逸分析技术?
- 3. 问:JVM如何判断对象是否逃逸?(了解就行)
- 4. 题外话:隐式使用同步方法是什么意思?
- 5. 有人说,单线程下的对象一定会发生锁消除吗?(好问题,我自己编的)
- 2. 锁粗化
- 1. 锁粗化的概念(for循环)
- 2. 锁粗化出现的场景(vector方法的add操作)
- 2. 锁粗化的原理
- 3. 锁升级
- 1. 锁升级的过程
- 2. 锁升级的原理
- 3. 什么时候会发生锁升级?
- 4. 自旋锁
- 1. 为什么要引入自旋锁?
- 2. 自旋锁的定义
- 3. 自旋锁的优点
- 4. 自旋锁的缺点
- 5. 适用场景
- 6. 使用自旋锁的注意事项
- 7.JVM相关参数
- 8. 问:自旋锁有什么缺陷?
- 5. 自适应自旋锁(自旋次数动态变化)
- 1. 自旋次数由什么决定?
- 2. 自适应自旋锁如何体现《自适应》?(JVM的伯乐心理,伯乐这词感觉也不是很恰当,又有点像是:教练海选种子选手)
- 6. 偏向锁(在单线程环境居多)
- 1. 什么是偏向锁?
- 2. 为什么要引入偏向锁?目的是什么?(使单线程快上再加快)
- 1. 思考题:一个线程执行同步块,那就是不会发生资源并发冲突,就代表着会发生锁消除吗?
- 3. 偏向锁的原理(一次CAS指令、对象头中的锁偏向线程ID)
- 1. 获取锁(拿到锁,然后记录在自己对象头中,且不会释放锁)
- 2. 释放锁
- 4. 偏向锁什么时候会转化成轻量级锁?(多线程且资源竞争激烈)
- 1. 题外话:关于资源竞争激烈,具体怎么解释?(这是概念的理解问题,到哪都可以问)
- 2. 多线程的情况下,一定不存在偏向锁吗?(这问题很深)
- 5. JVM相关配置
- 7. 轻量级锁
- 1. 轻量级锁的出现
- 2. 优点
- 3. 轻量级锁的原理(多次CAS指令)(这里有空再理解)
- 8. 重量级锁(如Synchronized、用户态、内核态)
- 1. 出现的原因
- 9.重量级锁、轻量级锁和偏向锁之间转换
- 1. 三者之间的转换
- 2. 三者之间的比较
锁
1. 锁的概念
锁消除、锁粗化、偏向锁、轻量级锁
2. 锁的状态
无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
3. 锁的优化策略
1. 锁消除(编译器的操作)
锁消除是在编译器级别的事情。
在编译器时,如果一些对象不可能存在共享资源竞争,JVM会消除这些对象的锁操作。
白话文:如果我不使用多线程,那就代表着我使用的这些对象,在编译期时会执行锁消除
1. 优点
节省时间,可以节省毫无意义的请求锁的时间
2. 锁消除的原理(怎么实现的?逃逸分析)
检查变量是否逃逸
- 不会锁消除:如果变量逃逸了,如方法使用了(显示或者隐式)synchronized修饰的方法
- 会锁消除:如果变量未逃逸,JVM会检测出来,并把它的锁去掉
1. 问:JVM如何在编译期消除锁的?
应该是在编译器生成字节码的时候,JVM阻止它去生成monitorEnter和monitorExit两个指令
2. 题外话:什么是逃逸分析技术?
这个我在【八股文】JVM篇有讲,比如方法中的一些对象没有返回或者没有被外部的方法使用,那这些对象就会直接保存在栈中,而不是堆!
3. 问:JVM如何判断对象是否逃逸?(了解就行)
对于虚拟机来说需要使用数据流分析
4. 题外话:隐式使用同步方法是什么意思?
对我们开发人员来说,虽然没有显示使用锁,但是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作(不就是加了synchronized嘛,搞得这么玄乎。。无语)。比如StringBuffer的append()方法,Vector的add()方法。
5. 有人说,单线程下的对象一定会发生锁消除吗?(好问题,我自己编的)
锁消除是根据对象逃逸分析的,单线程只能代表者不会发生并发冲突,但不代表着对象不会逃逸
2. 锁粗化
1. 锁粗化的概念(for循环)
就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
白话文:比如我对for循环里面的代码块进行了加锁操作,JVM为了提升效率,会直接把锁放到for循环外面,对整个for循环的代码块进行加锁操作
2. 锁粗化出现的场景(vector方法的add操作)
vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外
2. 锁粗化的原理
3. 锁升级
1. 锁升级的过程
锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
2. 锁升级的原理
锁的升级主要都是通过Mark Word中的锁标志位与是否是偏向锁标志位来实现的;
白话文:锁的升级主要是通过:对象头中锁相关的2个标志位(锁标志位和偏向锁标志位)实现的
3. 什么时候会发生锁升级?
4. 自旋锁
1. 为什么要引入自旋锁?
-
频繁的操作锁对CPU来说吃不消:对线程的阻塞和唤醒操作的实现,本质是CPU切换运行状态(用户态和内核态),操作太频繁的话CPU也吃不消呀
-
锁对象的时间非常短,不值得锁(有种杀鸡焉用牛刀的感觉):为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的
2. 自旋锁的定义
指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
白话文:尝试去获取锁,如果锁被占用,则循环检测,直到获取锁(应该有个最大检测时间吧)
3. 自旋锁的优点
- 避免线程切换带来的开销
白话文:我把CPU占用着,用完才释放
4. 自旋锁的缺点
- 占用CPU的处理时间,所以尽可能操作粒度小,否则会有种占着CPU不拉屎的感觉
5. 适用场景
- 适用于锁保护的临界区很小的情况(粒度小的)
6. 使用自旋锁的注意事项
- 保证自旋粒度小
- 需要设置最大循环次数
7.JVM相关参数
- -XX:+UseSpinning:在JDK 1.4.2中引入,默认关闭自旋,用这个命令开启自旋
- -XX:PreBlockSpin:JDK1.6中默认开启,且默认自旋10次,用这个命令修改自旋次数
白话文:我开启自旋,具体体现在哪里呢?是所有的对象监视器锁都采用了自旋锁??
8. 问:自旋锁有什么缺陷?
例子:线程A占用锁,线程B自旋10次未获取到锁,然后放弃了,不巧的是,线程B前脚刚走,线程A就好了,把锁释放了,呀!好尴尬呀。。
解决:JDK1.6引入自适应的自旋锁,夸一下,JDK1.6牛皮!
5. 自适应自旋锁(自旋次数动态变化)
1. 自旋次数由什么决定?
由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
白话文:参考上次的自旋时间
2. 自适应自旋锁如何体现《自适应》?(JVM的伯乐心理,伯乐这词感觉也不是很恰当,又有点像是:教练海选种子选手)
线程如果自旋成功了,那么下次自旋的次数会更加多,因为JVM认为既然上次成功了,下次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
白话文:JVM像是一个伯乐,如果有天赋(成功自旋一次),JVM会给你更多资源(更多的自旋次数),尽可能地保证你次次都自旋成功
反之,如果对于某个锁,很少有自旋能够成功,那么在以后要获取这个锁的时候自旋的次数会减少甚至省略掉自旋过程(为了省资源)
白话文:如果你没有天赋(很少能自旋成功),JVM会减少给你提供的资源,甚至不给你资源,活活饿死(别自旋了,阻塞去吧!)
6. 偏向锁(在单线程环境居多)
JDK1.6的时候引入的
1. 什么是偏向锁?
偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下,则一定会转化为轻量级锁或者重量级锁。
2. 为什么要引入偏向锁?目的是什么?(使单线程快上再加快)
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得(是因为设置了线程的优先级吗?不对线程的优先级是针对获取CPU调度而言的,和获取锁的优先级维度不一样,继续继续。。),为了让线程获得锁的代价更低(说的有点抽象了,兄dei),引进了偏向锁
白话文:在只有一个线程执行同步块时进一步提高性能
1. 思考题:一个线程执行同步块,那就是不会发生资源并发冲突,就代表着会发生锁消除吗?
错了,锁消除是根据对象逃逸分析的,不是通过判断资源是否有并发冲突
3. 偏向锁的原理(一次CAS指令、对象头中的锁偏向线程ID)
1. 获取锁(拿到锁,然后记录在自己对象头中,且不会释放锁)
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可
白话文:一个线程第一次进入同步块,会在对象头中记录锁偏向线程ID,下次再访问的时候,如果还是偏向锁,就会先判断是不是这个锁偏向线程ID,是的话就直接执行同步代码块
2. 释放锁
偏向锁的释放采用了 一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要 等待全局安全点(这个时间点是上没有正在执行的代码)
当发生线程竞争资源时:
-
暂停拥有偏向锁的线程
-
判断锁对象是否还处于被锁定状态(看看偏向锁的这个线程还在不在用):
否:恢复到无锁状态(01),允许其他线程竞争,谁抢到,谁就是新的轻量级锁
是:升级为轻量级锁状态(00),进入轻量级锁的竞争模式
图片链接:https://www.cnblogs.com/aspirant/p/11470858.html
图片最下面那部分不太准备,看我上面撤销锁的文字部分好了
4. 偏向锁什么时候会转化成轻量级锁?(多线程且资源竞争激烈)
多线程情况下,出现资源竞争比较激烈的时候,就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗也必须小于接下来的CAS原子指令的性能消耗
白话文:多线程且资源竞争不激烈的情况下
1. 题外话:关于资源竞争激烈,具体怎么解释?(这是概念的理解问题,到哪都可以问)
即线程A尚未执行完同步代码块,线程B发起了申请锁的申请
2. 多线程的情况下,一定不存在偏向锁吗?(这问题很深)
不一定,如果资源竞争不激烈的话,还是继续用偏向锁
白话文:偏向锁处理的时间 < 偏向锁转成轻量级锁的时间
在 JDK 1.6 中默认是开启偏向锁和轻量级锁的
5. JVM相关配置
- -XX:-UseBiasedLocking:禁用偏向锁(jdk1.6默认是开启的,应该是设置为false代表禁止)
7. 轻量级锁
1. 轻量级锁的出现
当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
2. 优点
- 减少传统的重量级锁使用操作系统互斥量产生的性能消耗
- 靠多次CAS实现的,效率高
3. 轻量级锁的原理(多次CAS指令)(这里有空再理解)
- 在线程进入同步块时,如果同步对象锁状态为无锁状态,JVM在栈帧中创建锁记录空间(Lock Record),此时线程堆栈与对象头的状态如下图所示:
- 把对象头中的MarkWord复制到锁记录空间中
- 拷贝成功后,JVM将使用CAS操作尝试将对象MarkWord中的LockWord更新为指向当前线程LockRecord的指针,
- 如果CAS成功:把复制过来的MarkWord,更新回对象的MarkWord,这样就获取到对象的轻量级锁了
- 如果CAS失败:自旋执行,自旋结束时仍未获得锁,膨胀为重量级锁
图片来源:https://www.cnblogs.com/aspirant/p/11470858.html
8. 重量级锁(如Synchronized、用户态、内核态)
Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。
1. 出现的原因
如果线程通过一定次数的CAS尝试没有成功,则进入重量级锁
9.重量级锁、轻量级锁和偏向锁之间转换
1. 三者之间的转换
图片来源:https://www.cnblogs.com/aspirant/p/11470858.html
2. 三者之间的比较
图片来源:https://www.cnblogs.com/aspirant/p/11470858.html
这篇关于【多线程】锁消除、锁粗化、偏向锁、自旋锁、自适应字段锁、轻量级锁、重量级锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!