无锁、偏向锁、轻量级锁、重量级锁,完整的锁升级!
在打开这篇博客以前,我相信你对synchronized关键字是有一定的知识储备,本文主要带你强化记忆整个锁升级的过程,希望对你有帮助。当然我也相信,如果你真的理解了这些内容,你又会发现自己要学的东西还有很多!!!
文章目录
- 1、锁升级基本描述
- 2、关于锁的疑问
- 2、结合代码进行分析测试
- 2.1、测试对象头的锁情况
- 2.2、测试hashCode值
- 3、概念补充
1、锁升级基本描述
synchronized关键字是Java里面用于防止多线程下资源冲突所带来的一系列问题。使用synchronized关键字以后,我们就可以将所有的线程排好队,先拿到锁的先执行。这就可以很好的解决多线程下的资源冲突问题。
可是随着技术的发展,人们对于性能的要求越来越高,传统的排队的方式,已经无法满足我们的需求。
于是在JDK1.6以后,synchronized关键字进行了优化,且引入了无锁、偏向锁、轻量级锁、重量级锁等概念。由无锁变为重量级锁的一个过程叫做锁升级。
无锁:我们刚实例化一个对象
偏向锁:单个线程的时候,会开启偏向锁。可以使用-XX:-UseBiasedLocking来禁用偏向锁。
轻量级锁:当多个线程来竞争的时候,偏向锁会进行一个升级,升级为轻量级锁(内部是自旋锁),因为轻量级锁认为,我马上就会拿到锁,所以以自旋的方式,等待线程释放锁
重量级锁:由于轻量级锁过于乐观,结果迟迟拿不到锁,所以就不断地自旋,自旋到一定的次数,为了避免资源的浪费,就升级为我们最终的自旋锁。
2、关于锁的疑问
我们不妨大胆的猜测一下,他升级的整个过程。然后提出疑问:
- 我怎么知道我现在是什么锁?
- 锁的内部结构是怎么样的?
- 锁可以跳级吗?
回答:
1、2问:当我们实例化一个对象的时候,对象 = 对象头 + 对象实例数据(instance data) + 对齐填充(padding),对象头 = 对象运行时数据(mark word 8个字节) + 对象类型指针(class pointer 由8个字节压缩为4个字节)+ (如果是数组就还需要4个字节)。其实我们不难发现,无论对象类型的指针是否压缩,在不是数组的情况下,对象头都是占16个字节,因为8+4的时候,为了数据对齐会自动填充4位。我们的锁对应的标识,是存放在mark word里面。mark word的8个字节的详细信息,如下图所示:
3问:我们可以进行对应的参数,使我们的对象直接跳过偏向锁,进入轻量级锁。如果我们在一个实现就知道存在多线程竞争的环境中,我们使用偏向锁的意义就不是很大,因为注定了它会生成轻量级锁,甚至更高。网上也有一种说法是:一次偏向锁的撤销操作带来的性能损耗一定要小于轻量级锁自旋一次的性能损耗,不然我们为什么不在没有线程竞争的环境下,直接使用偏向锁,反正也要升级。具体的锁变化的过程,如下图所示:
这两张图建议记住
2、结合代码进行分析测试
其实前面的讲解,我们对锁升级已经有了一个比较深刻的理解了,但是为了强化记忆,我们还是实际上手一下代码,毕竟空洞的文字容易忘记
此处我们需要引入Java Object Layout这个工具,它是用于打印对象的布局的,使用方式,直接引入对应的maven依赖:
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version>
</dependency>
2.1、测试对象头的锁情况
测试代码:
package pers.mobian.synchronizedtest;import org.openjdk.jol.info.ClassLayout;public class Test01 {public static void main(String[] args) {Object o = new Object();//打印加锁之前的字节信息System.out.println(ClassLayout.parseInstance(o).toPrintable());//为了控制变量,我们再打印一次信息System.out.println(ClassLayout.parseInstance(o).toPrintable());synchronized (o){//打印加锁之后的字节信息System.out.println(ClassLayout.parseInstance(o).toPrintable());}}
}
测试结果:
通过图片,我们不难发现,加锁以前的字节信息和加锁之后的字节信息是发生了改变的。并且变化是在前8个字节,8-11位的字节码没有变化,那么就可以确定我们前面说的,加锁实际上是改变了我们对象头中对象运行时数据(mark word)那8个字节
2.2、测试hashCode值
根据前面的测试,如果我们不显式的调用hachCode,该空间的值是不会发生变化的
测试代码:
package pers.mobian.synchronizedtest;import org.openjdk.jol.info.ClassLayout;public class Test01 {public static void main(String[] args) {Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());//显式调用hashCode方法o.hashCode();System.out.println(ClassLayout.parseInstance(o).toPrintable());}
}
执行结果:
我们的8-11位的字节依然没有变化。0-7在没有加锁的情况下,还是变化了,这是由于我们显式的调用了hashCode方法,继而修改了相应的值。通过这个实验我们也能够确定hashCode的计算也是放在对象头的mark word中,且需要显式调用才能有变化
3、概念补充
1、为什么有自旋锁还需要重量级锁?
自旋会不断的消耗CPU资源,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。到了重量级锁以后,由于重量级锁有等待队列,拿不到锁的进入等待队列,不需要消耗CPU资源
2、偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁,JVM启动过程,会有很多线程竞争,所以默认情况启动时不打开偏向锁,过一段儿时间再打开。
3、不同锁的不同标志形式
锁状态 | 偏向锁位 | 锁标志位 |
---|---|---|
无锁状态 | 0 | 0 1 |
偏向锁 | 1 | 0 1 |
轻量级锁 | 无 | 0 0 |
重量级锁 | 无 | 1 0 |
4、轻量级锁也叫自旋锁和无锁
自旋锁我们很好理解,轻量级锁内部不断地自旋,以求获取到对应的锁资源。这里的无锁,我们可以理解为,因为这个锁它在不断的自旋,没有真正的拿到锁,所以也可以叫做无锁。
5、轻量级锁的相关补充
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
适应性自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
6、synchronized重量级锁的相关补充
synchronized包含6个核心组件:Wait Set、Contention List、Entry List、OnDeck、Owner、!Owner
Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
Owner:当前已经获取到所资源的线程被称为 Owner;
!Owner:当前释放锁的线程。
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
- Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
- 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
- synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的(程序在操作系统的内核态与用户态之间切换),有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
- JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
这个过程也完完全全说明了synchronized是一个非公平的锁
无锁、偏向锁、轻量级锁、重量级锁,完整的锁升级!相关推荐
- java多线程之锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
转载至:https://blog.csdn.net/zqz_zqz/article/details/70233767 之前做过一个测试,详情见这篇文章<多线程 +1操作的几种实现方式,及效率对比 ...
- java轻量级和重量级_Java 偏向锁、轻量级锁和重量级锁
前言 最开始听到偏向锁.轻量级锁和重量级锁的概念的时候,我还以为是 Java 中提供了相应的类库来实现的,结果了解后才发现, 这三个原来是虚拟机底层对 synchronized 代码块的不同加锁方式. ...
- Java锁---偏向锁、轻量级锁、自旋锁、重量级锁
Java锁-偏向锁.轻量级锁.自旋锁.重量级锁 之前做过一个测试,反复执行过多次,发现结果是一样的: 单线程下synchronized效率最高(当时感觉它的效率应该是最差才对): AtomicInte ...
- 轻量级锁_一句话撸完重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁 不看后悔系列...
重量级锁?自旋锁?自适应自旋锁?轻量级锁?偏向锁?悲观锁?乐观锁?执行一个方法咋这么辛苦,到处都是锁. 今天这篇文章,给大家普及下这些锁究竟是啥,他们的由来,他们之间有啥关系,有啥区别. 重量级锁 如 ...
- 1.6的锁优化(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)
高效并发是JDK 1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning).锁削除(Lock Elimin ...
- 并发系列三:证明分代年龄、无锁、偏向锁、轻量锁、重(chong)偏向、重(chong)轻量、重量锁
前言 上篇文章咱们了解了synchronized关键字的常见用法.对象头以及证明了一个对象在无锁状态下的对象头markwork部分的前56位存储的是hashcode.接下来,咱们继续来根据对象头分别证 ...
- jvm第7节-锁(偏向锁,轻量锁,自旋锁)
为什么80%的码农都做不了架构师?>>> 在介绍锁之前我们先介绍一个线程不安全的例子,一个全局的list,开2个线程往里面插入数据,代码如下: package com.jvm. ...
- java的轻量锁,jvm第7节-锁(偏向锁,轻量锁,自旋锁)
在介绍锁之前我们先介绍一个线程不安全的例子,一个全局的list,开2个线程往里面插入数据,代码如下: package com.jvm.day6.lock.demo; import java.util. ...
- 【剧前爆米花--爪哇岛寻宝】常见的锁策略——乐观锁、读写锁、重量级锁、自旋锁、公平锁、可重入锁等
作者:困了电视剧 专栏:<JavaEE初阶> 文章分布:这是关于操作系统锁策略的文章,包括乐观锁.读写锁.重量级锁.自旋锁.公平锁.可重入锁等,希望对你有所帮助! 目录 乐观锁和悲观锁 悲 ...
- 【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿
多把锁的应用 减小锁粒度,提交并发度. package com.bo.threadstudy.four;import lombok.extern.slf4j.Slf4j;/*** 多把锁的情况,以及后 ...
最新文章
- Python实现一元及多元线性回归
- 【PHPWord】TextRun
- 20080509 - System.ExecutionEngineException 在 DefaultDomain 中发生
- oracle,导入,导出数据
- SQL中Case和convert()
- 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。...
- 配置DATAGUARD 时关于 LOG_FILE_NAME_CONVERT配置错误的解决
- .NetCore(四) 在Nginx部署
- TypeScript中怎么用接口(interface)描述类(静态部分与实例部分)
- 双向链表的操作(创建,插入,删除)
- 三进制计算机_“九章”量子计算机这么猛,到底能做啥?只为了一条公式的结果吗...
- 设计,构建线框图和对Android应用进行原型制作:第1部分
- 大数据_02【大数据基础知识】
- 【供应链】全面分析供应链类型
- 国产android系统,最干净的国产安卓7.0系统,体验还不错
- 1^2+2^2+…+n^2求和公式推导
- windows端口被占用的解决方法
- css上下左右居中得几种方法
- sequence 序列
- 如何解决AutoRunner的脚本回放报对象不存在问题
热门文章
- Eclipse创建并运行Java程序输出Hello World
- setFitView的zoom只是整数,导致缩放尺寸不合适的解决方案
- Linux安装Prometheus
- createdroptargets_拖拽神器React DnD你真的了解了吗?
- stomp+websocket 集群问题_手把手搭建WebSocket多人在线聊天室
- microsoft sql server无法删除_分享一则生产数据库sql优化案例:从无法删除到耗时20秒
- python字节码解析_dis --- Python 字节码反汇编器 — Python 3.9.1 文档
- Spring中实现AOP的三种方式
- android 动态创建数据库表,简析Android数据库中创建表与LitePal的基本用法
- python做什么生意好找_寻找python项目来提高你的技能