目录
1、详解对象头结构
对象是存放在堆内存中的,对象大致可以分为三个部分:对象头、实例变量和对齐填充
一个普通Java对象的对象头结构:
以32为虚拟机为例
- Mark Word用于存储对象自身的运行时数据
- klass word指向对象的类型
64位虚拟机的mark word占8个字节,它的具体内容:
- 普通对象:状态位01,偏向标识为0,主要存放对象的hashcode、分代年龄
- 偏向锁:状态位01,偏向标识为1,存放偏向的线程id、分代年龄
- 轻量级锁:状态位00,存放对应的lock record锁记录对象在栈上的地址
- 重量级锁:状态位10,存放对应的monitor的地址
- 等待被垃圾回收,状态位11
可以看出,对象在作为锁使用时,与该对象的含义、继承关系等都无关,因为锁的底层只在该对象上体现。
注意:一个对象作为synchronized的锁对象,它就不会被垃圾回收,后续作为GC Roots。所以它不需要存储分代年龄。
2、重量级锁的实现原理
1、monitor
monitor,翻译为“监视器”或“管程”。
Monitor对象是操作系统提供的,每个Java对象都可以关联一个Monitor对象,它是用来给对象上锁的,主要分为以下三个部分。
- WaitSet:存储 之前获取过这把锁,但进入Waiting状态的线程。
- EntryList:阻塞队列,存放等待获取锁的线程
- Owner:当前持有锁的线程
另外还有count属性用来表示锁重入。
2、给一个对象上锁的原理
使用synchorzed给对象上重量级锁后:
- 该对象头的Mark Word中,存放指向关联的Monitor对象的指针。该对象原有的mark word信息保存在monitor中。
- 本来对象是normal的。
- 使用synchorzed上锁后,对象状态变为Heavyweight Locked,mark word改为存储Monitor对象的地址。
- mark word后两位从01(普通对象)变为10(重量级锁)。
- monitor的owner存储了当前持有锁的线程。
3、重量级锁的锁重入
在monitor中,有一个count属性,用于记录锁重入的次数。
- 使用synchorzed给对象上锁,对象头的mark word部分存储了一个Monitor对象的地址,此时Monitor的Owner为空,count为0
- 一个线程尝试获取锁
- 先判断count属性是否为0,如果为0,说明锁可用。
- 把Owner设置为 本线程,线程 进入Running 状态。
- count属性+1
- 如果count属性不为0
- 检查锁对象的对象头,发现它是Heavyweight Locked状态,关联了一个Monitor
- 查看Monitor的Owner属性,如果owner属性指向本线程,说明是锁重入,count+1
- 如果owner属性指向其他线程,当前线程被阻塞,进入阻塞队列 EntryList,变为 Blocking 状态。
- 等到持有锁的线程执行完毕,就会唤醒 EntryList 中阻塞的线程来竞争锁。
- 如果持有锁的线程在运行中调用了wait()方法,就释放锁,进入monitor的等待队列。
syn 修饰的内容如何获取monitor对象
syn 修饰代码块
JVM在需要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令。
JVM确保它们是成对出现的,但是根据程序执行的不同分支,比如正常结束或出现异常,一个monitorentry下面可能有多个monitorexit。但只会执行到一个。
这两条指令依赖于底层的操作系统的Mutex Lock来实现。
使用Mutex Lock需要将当前线程挂起,并从用户态切换到内核态来执行,这种切换的代价非常高。
执行monitorentry,含义是尝试获取monitor的所有权。
syn 修饰方法
方法同步不再是通过插入monitorentry和monitorexit指令实现。
是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的。
如果方法表(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,说明这个方法是一个同步方法。
那么线程在执行方法前会先去获取对象的monitor对象。
如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。
6、重量级锁的缺陷
Java中的线程需要映射到操作系统的原生线程之上。
在早期的Java版本中,synchrozed的底层只能使用monitor,这是依赖于操作系统的Mutex Lock(互斥锁)实现的。
每次对线程的唤醒和阻塞,都必须经由操作系统转入内核态来完成,这是一个重量级操作,效率比较低。
3、synchrozed 的优化
montior 是由操作系统提供的,成本较高。如果每次进入 synchrozed时都需要进入 montior 中获取锁,性能较低。
JDK1.6开始,对synchronized做了优化,引入了偏向锁、轻量级锁。
它们的引入是为了解决,在没有多线程竞争或基本没有竞争的场景下,因使用传统锁机制带来的繁重性能开销问题。
1、偏向锁概述
大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁。
因此,如果每次都要竞争锁,会有很多额外性能损失、为了降低获取锁的代价,引入了偏向锁。
比如一段同步代码块,假如每次都是同一个线程去执行它,其实是线程安全的,没有必要去竞争锁。
偏向锁的优化思想是:
- 如果使用轻量级锁,每次执行同步代码之前都需要使用CAS操作,效率较低
- 偏向锁的思想,每次执行同步代码之前只需要检查偏向线程ID即可,需要的CAS操作较少,所以比起轻量级锁效率更高
- 但是偏向锁在存在多线程执行的情况虾,需要发生偏向锁撤销,这个操作的开销必须小于节省CAS带来的开销,否则这个优化就没有意义
偏向锁的获取流程
- 从当前线程的栈中找到一个空闲的Lock Record,指向当前的锁对象
- 判断锁对象的mark word的低三位是否为101偏向状态,如果是:
- 判断偏向的线程ID和当前线程ID是否相同
- 如果相同,就直接执行同步代码块
- 如果是匿名偏向,即偏向状态但是偏向线程ID为空,就使用CAS将当前线程ID置为偏向线程ID
- 如果修改成功,就成功获取偏向锁
- 如果修改失败,就升级为轻量级锁
- 如果不相同,就查看锁对象头中存储的线程id对应的线程是否存活。
- 检查偏向线程的状态,如果线程已经死亡,就查看是否允许重偏向
- 如果允许重偏向,就把锁对象设置为匿名偏向状态,之后进行重偏向
- 如果不允许重偏向,就把锁对象设置为无锁状态,下次只能直接升级成轻量级锁
- 如果偏向线程依然存活,就需要在下一个全局安全点STW,查看该线程的栈帧信息
- 如果该线程不再使用该锁对象,就查看是否允许重偏向
- 如果允许重偏向,就把锁对象设置为匿名偏向状态,之后进行重偏向
- 如果不允许重偏向,就把锁对象设置为无锁状态,下次只能直接升级成轻量级锁
- 如果该线程还需要持有偏向锁,就撤销偏向锁,设置为轻量级锁
- 具体该线程是否使用偏向锁,可以遍历它的栈帧中的Lock Record对象来判断。
- 如果该线程不再使用该锁对象,就查看是否允许重偏向
- 检查偏向线程的状态,如果线程已经死亡,就查看是否允许重偏向
- 判断偏向的线程ID和当前线程ID是否相同
- 如果不是,就进入轻量级锁的逻辑,构造一个无锁状态的对象头mark word,然后存入Lock Record中
偏向锁解锁后,不会被主动释放:一个线程退出同步代码块后,不会撤销锁对象中自己的线程id。
2、轻量级锁概述
情形:对于一个对象,在多线程场景下,多个线程并不会同时访问它,而是错开访问
。
产生了两个问题:
- 通常情况下不存在竞争,此时其实不用加锁。每次线程都要先访问montior,才能执行它,效率就很低。
- 但是,每个线程在执行它之前应该先检查,它是否正在被其他线程运行,否则会出现问题
思想:
- 使用某种标记,能快速判断是否有其他线程持有该锁对象,表明当前是否存在竞争关系。如果不存在竞争,说明只有一个线程访问,就不用经过锁
- 如果发现存在竞争,再去使用锁(轻量级锁升级为重量级锁)
轻量级锁的获取流程
轻量级锁对于编码人员是透明的(不可见),语法仍然是synchrozed ,底层会自动优化。
Lock Record
锁记录对象,放在线程的栈帧中。
它包含两部分信息:
- 存放锁对象头的mark word
- 存放锁对象的地址信息
一个线程试图获取轻量级锁的流程
-
如果锁对象为 001 无锁状态,就在线程的栈帧中创建一个 Lock Record 锁记录对象。
-
将锁对象的mark word拷贝到本线程的Lock Record中,称之为 Displaced Mark Word
-
尝试获取锁
- 使用CAS操作,试图将锁对象头中mark word的内容,替换成该线程的Lock Record的地址。
-
如果CAS执行成功,当前线程成功获取锁
- 对象头的Mark Word中存储了锁记录地址和状态 00 (00表示轻量级锁),表示由该线程给对象加锁
- 对象头中原先的信息,由锁记录临时保存。等到释放锁的时候,再把数据恢复回去。
-
如果 cas 替换失败,那么判断对象头Mark Word的“lock record 锁记录地址”,存在两种情况:
-
锁记录地址指向其他线程,说明是其它线程已经持有了该 锁对象 的轻量级锁,当前线程先自旋。如果达到了自旋次数,或者又有新的线程来竞争锁,就将轻量级锁膨胀为重量级锁,锁对象头部指向一个monitor。
-
锁记录地址指向此线程,说明是此线程自身执行了 synchronized 锁重入,会再添加一条 Lock Record 锁记录,作为重入的计数。
这样设计是很有必要的,因为多次持有锁时如果不额外做记录,在释放锁时就会出现问题。只有等到该线程的锁全部被释放,才说明当前线程的操作已经结束,该对象才能被允许再次上锁。
-
-
当退出 synchronized 代码块释放锁时,如果有值为 null 的锁记录,表示有重入,这时会删除该条锁记录,表示重入计数减一
当退出 synchronized 代码块释放锁时,如果锁记录的值不为null,则使用cas,将对象头Mark Word的值恢复
- 如果操作成功,则成功释放了该对象的锁
- 如果操作失败,说明该轻量级锁进行了锁膨胀,或者已经升级为了重量级锁,就进入重量级锁的解锁流程。
4、总结 syn的锁升级过程
一个线程要获取一个锁对象的锁:
- 检查是否是对象mark word后三位状态位是否是001无锁状态,其中第一位表示是否偏向,后两位表示对象的类型
- 如果是,就设置为101偏向锁,通过CAS将自己的线程id放入对象头的mark word,此时锁对象偏向当前线程
- 如果对象状态是101,检查mark word的线程id是否和当前线程id相同。
- 如果相同,不用获取锁,直接执行被锁住的代码
- 如果不相同,主动检查持有偏向锁的线程状态。如果它已死亡,或者不需要偏向锁了,就将锁对象置为001无锁状态。否则,如果该线程还在使用偏向锁,当前线程就将偏向锁升级为轻量级锁,锁对象状态为00
- 如果对象状态是00,使用CAS,将锁对象头中mark word的内容,替换成当前线程的Lock Record的地址
- 如果操作成功,当前线程成功持有轻量级锁
- 如果操作失败,就尝试自旋。如果自旋到一定次数依然操作失败,或者有别的线程尝试获取轻量级锁,就将轻量级锁升级为重量级锁。申请一个monitor,将对象状态置为10,mark word指向monitor对象,当前线程进入monitor的阻塞队列等待。
锁只能升级不能降级,但是偏向锁状态可以被重置为无锁状态,后续升级成轻量级锁。
原因是,既然锁升级了,那么说明会有高并发的场景出现,那么肯定不能擅自降级,因为不能预测是否还有高并发的时刻。
今天的文章偏向锁 轻量级锁 重量级锁区别_极窄门锁分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/84584.html