java对象的结构:

1646818977749.png

1.对象头:
  1. ①:Mark Wordhashcode、分代年龄、状态、加锁状态位
  2. ②:Klass word:类型指针(什么类型的对象)

2.对象体:
  1. 成员变量的信息

Java 对象头:

以 32 位虚拟机为例

1646816188296.png

64 位虚拟机 Mark Word

1646816210489.png

Monitor概念

Monitor被翻译为监视器或者管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor的指针

Monitor结构如下:

1646817043063.png

1.刚开始Monitor种Owner为null

2.当Thread-2执行synchronized(obj) 就会将Monitor的所有者Owner置为Thread-2,Monitor种只能有一个Owner

3,在Thread-2上锁的过程种,如果Thread-3,4,5也来执行synchronized(obj) ,就会进入EntryList BLOCKED

4.Thread-2执行完同步代码块的内容,然后唤醒EntryList种的等待线程来竞争锁,竞争是非公平的

5.图中WaitSet种的Thread-0,Thread-1是之前获得过锁,当条件不满足WAITING状态的线程,后面讲wait-notify

时分析

  1. **注意:**
  2. synchronized必须时进入同一个monitor才有上述效果
  3. 不加synchronized的对象不会关联监视器,不遵从以上规则

synchronized原理进阶:

轻量级锁:

使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁

轻量级锁对使用者是透明的,语法仍然为synchronized

  1. 1.创建锁记录(Lock Record),每个线程的栈帧都会包含一个锁的记录,内存可以存储锁对象的Mark Word

1646819269864.png

  1. 2.让锁记录中Object reference 指向锁对象,并且尝试用cas替换Object中的Mark Word,将Mark Word的值存入锁记录(若Mark Word值已经被其他线程所修改则替换失败)

1646819423103.png

  1. 3.如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁

1646819563598.png

如果cas失败:
  1. 1.其他线程已经持有了该Object的轻量级锁,此时表明有竞争,进入锁膨胀过程
  2. 2.如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数<br />![1646820022734.png](https://cdn.nlark.com/yuque/0/2022/png/26737039/1647331695203-2b372579-5ee7-4efc-ae87-c2252e2679c6.png#clientId=u46ad077f-3ef3-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u733d9fb1&margin=%5Bobject%20Object%5D&name=1646820022734.png&originHeight=359&originWidth=597&originalType=binary&ratio=1&rotation=0&showTitle=false&size=109184&status=done&style=none&taskId=u75c465ef-1c37-456f-8a81-ec82f3f14d3&title=)

解锁:
  1. 1.当退出synchronized代码块(解锁时) 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1

1646820139830.png

  1. 2.当退出synchronized代码块时,锁记录的值不为null,这是使用casMark Word的值恢复给对象头
  2. 成功,则解锁成功
  3. 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

2.锁膨胀:

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

1.当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

1646820530427.png

2.这时Thread-1加轻量级锁失败,进入锁膨胀流程
  1. ①:即为Object对象申请Monitor锁,让Object指向重量级锁地址(后两位数字变为10
  2. ②:然后自己进入MonitorEntryList BLOCKED

1646820663799.png

3.当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败,这时会进入重量级解锁流程:即按照Monitor地址找到Monitor对象,将Owner设置为null,唤醒EntryList中的BLOCKED线程

3.自旋优化:(比较适合多核CPU)

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持有锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞 (阻塞会发生上下文切换,消耗性能)

自旋成功的情况:

1646823854008.png

自旋失败的情况:

1646823915727.png

注意:

1.在java6之后自旋锁是自适应的,比如对象刚刚的一次自选操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就是少自旋甚至不自旋,比较只能

2.自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势

3.java 7 以后不能控制是否开启自旋功能


4.偏向锁:

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作(性能损耗)

java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就没有表示竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有

1646824479431.png

4.1 偏向状态:

回忆对象头格式:

1646824815055.png

一个对象创建时:

  1. 1.如果开启了偏向锁(默认开启),那么对象创建后,markword值的后三位为101(biased_lock字段表示是否开启偏向锁,1为开启),此时他的threadepocage都为0
  2. 2.偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加参数来禁用延迟
  3. 3.如果没有开启偏向锁,那么对象创建后,markword值为001,此时他的hashcodeage、都为0,第一次用到hashcode时才会赋值
  4. 4.处于偏向锁的对象解锁后,线程id仍存储于对象头中

注意:

  1. 当我们的程序本身就是线程很多,那么偏向锁就不适用,可以通过参数来禁用:
  2. 添加VM参数: -xx:-UseBiasedLocking来禁用偏向锁

4.2 撤销偏向锁—-调用对象的hashcode方法

当我们调用对象的hashcode方法就会禁用掉偏向锁

原因:通过对象头的结构可知,Mark Word 的空间是有限的,当我们开启偏向锁,就会存储线程id,而没有空间存储hashcode,所以我们调用hashcode方法,就会关闭偏向锁,清除掉线程id替换为hashcode

为什么轻量级锁和重量级锁不会有上述问题?
  1. 轻量级锁会存储在线程栈帧的锁记录中
  2. 重量级锁会将hashcode存储在Monitor对象中,解锁时再进行还原
  3. 只有偏向锁没有额外的空间,所以会有上述问题

4.3 撤销偏向锁—-其他线程使用对象

当其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

4.4 撤销偏向锁—-调用wait/notify

只有重量级锁才有这两个方法,所以调用会将偏向锁或轻量级锁转换为重量级锁

4.5 批量重偏向

如果对象虽然被多个线程访问,但是没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID(线程ID)

当撤销偏向锁的阈值超过了20次,jvm会觉得是不是偏向错了呢?于是会给在这些对象加锁时重新偏向至加锁线程

4.6 批量撤销

当撤销偏向锁的阈值超过了40次,jvm会觉得自己偏向错了,根本就不该偏向,就会把整个类的所有对象变为不可偏向的,新建的对象也是不可偏向的

5.锁消除

java运行时有一个JIT即时编译器,会对java字节码进一步优化,反复运行的代码超过一定的阈值就会进行即时优化,当发现变量不会存在线程安全问题,JIT就会把多余的synchronized去掉
1646830802690.png


6.锁的粗化

  1. public void test1(){
  2. for(int i=0;i<1000;i++){
  3. synchronized(Test.class){
  4. sout("hello");
  5. }
  6. }
  7. }

锁粗化后:扩大锁的范围,避免反复加锁和释放锁

  1. public void test1(){
  2. synchronized(Test.class){
  3. for(int i=0;i<1000;i++){
  4. sout("hello");
  5. }
  6. }
  7. }