synchronized锁详解

1.对象内存布局

在这里插入图片描述

对象头 MarkWord

对象头,8字节。包括了对象的hashCode、对象的分代年龄、锁标志位等。结构如下图所示:
在这里插入图片描述

类指针classPointer

对象指向它的类元素的指针。在不开启对象指针压缩的情况下是8字节。压缩后变为4字节,默认压缩。 通过命令:java
-XX:+PrintCommandLineFlags -version 查看classPointer是否开启压缩 该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。Java对象的类数据保存在方法区。
对象可以通过此类指针获取类的位置。

数组长度

在数组情况下有此部分

实例数据 Instance Data

存放指向数据的指针,压缩情况下一般为4字节

对齐补充 Padding

用于对象在内存中占用的字节数不能被8整除的情况下,进行补充。

例题补充

1.Object o = new Object()在内存中占了多少字节?
markword 8字节, 因为java默认使用了calssPointer压缩,classpointer 4字节, padding 4字节 因此是16字节。
如果没开启classpointer默认压缩,markword 8字节,classpointer 8字节,padding 0字节,也是16字节。
2.User (int id,String name) User u = new User(1,‘张三’);占用多少字节 markword 8字节?
开启classPointer压缩 ,classpointer 4字节, instance data int 4字节(int基本数据类型存储数据为4个字节) 开启普通对象指针压缩 ,String 4字节,(String类型非基本数据类型,存储对象的指针,指针压缩后为4个字节) padding 4字节, 一共24字节。

2.synchronized的锁状态

在这里插入图片描述

2.1偏向锁

偏向锁定义

在同步代码块中,大部分情况下,并不会有多个线程并发执行,很大部分是单个线程反复请求执行。这种情况下直接使用重量级锁是很耗费资源的。

在这里插入图片描述偏向锁加锁过程

执行字节码指令mornitorEnter前。
1.向当前线程栈内,添加一条锁记录Lock Record ,锁记录的锁引用指向当前锁对象。(不需要CAS操作,栈内操作不涉及并发)
2.通过CAS设置锁对象的MarkWord,存储当前线程id。(锁对象可能存在并发操作,所以使用CAS)

偏向锁释放锁过程

偏向锁退出同步代码块字节码指令mornitorExit。
1.将当遍历前线程栈,并与当前锁对象相关的所有锁记录Lock record都拿到,需要修复锁记录和Markword,使其变成无锁状态。
2.再检查锁对象MarkWord,当前锁对象是偏向锁状态,就不用管,完成了。(锁对象保留了偏向线程的偏向锁,好处是,下此获取偏向锁时,对比一下线程ID,添加一条锁记录就ok了,不涉及一条CAS操作)

2.2轻量级锁

轻量级锁定义

轻量级锁是多线程交替执行,不存在并发执行现象。

MarkWord中轻量级锁状态:

轻量级锁加锁过程

(1)向当前线程栈内添加一条锁记录Lock record,锁记录的锁引用Owner指向当前锁对象地址。
(2)当前锁对象生成一个无锁状态的MarkWord值,即displaceMarkWord,并把displaceMarkWord的值保存在锁记录的displace字段中。
(3)使用CAS设置当前锁对象的MarkWord值为轻量级锁状态,保存指向栈中锁记录的指针。(无锁状态下,CAS一定会设置成功;但已经处于轻量级锁状态时,会这是失败,此状态有两种情况1.相同线程锁重入,锁记录中的displace字段置为null即可。2.不同线程 锁升级)。
(4)将锁对象的对象头的MarkWord替换为指向锁记录的指针.

线程栈内指向当前锁对象的所有锁记录,既是锁重入的次数。

轻量级锁释放锁过程

1.从当前线程栈中找到“最后一条”,指向当前锁对象的锁记录,把锁引用字段置为null(如果displace字段为null,则是重入锁记录)
2.通过CAS设置锁对象的MarkWord值,设置为锁记录中的displaceMarkWord值,为无锁状态,轻量级锁释放成功。(CAS失败,表示锁在膨胀中或者是重量级锁)

轻量级锁主要有两种

1.自旋锁
2.自适应自旋锁

在这里插入图片描述

2.3 重量级锁

为什么说重量级锁开销大呢?
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
这就是说为什么重量级线程开销很大的。

重量级锁定义

1.重量级锁是为了解决多线程并发执行情况下的锁。通过管程对象ObjectMornitor实现.

2.MarkWord中重量级锁状态:
在这里插入图片描述
3.管程模型ObjectMornitor对象,C++实现
在这里插入图片描述
4. 每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

重量级锁加锁过程

①检擦MarkWord中重量级锁状态和指向管程对象的指针。
②抢占锁的线程到管程通过CAS操作,自旋的方式将owner字段设置成当前线程(轻升级到重时,设置的是持有轻的线程),设置成功返回,不成功说明有线程抢用。
③如果通过CAS抢占锁失败,当前线程会变成block状态,进入同步队列entryList中,当访问 Object
的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。 等待再次竞争锁。
④当前线程调用wait方法时会封装成waiter节点,存入WaitSet队列中,等待被唤醒,notify,notifyAll。

重量级锁释放锁过程

1.将ObjectMorniter的字段设置为null
2.检查竞争队列和EntryList是否有等待线程,有的话唤醒去抢占锁。

附加例题

1.什么情况下会有锁膨胀操作?

  1. 线程调用轻量级锁或偏向锁的hashCode方法时,会膨胀为重量级锁。(因为偏向锁和轻量级锁状态下,MarkWord没办法存储hash值)
  2. 持锁线程调用wait()方法,会锁膨胀为重量级锁(因为偏向锁和轻量级锁,没有管程对象,不能存放线程节点)
  3. 在轻量级锁状态下,有并发竞争,锁膨胀为重量级锁。(此时会获取一个空管程对象,通过CAS设置MarkWord为膨胀状态,CAS设置管程owner字段为当前的线程。)

注:

  1. 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;

  2. 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;

  3. 重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。

  4. 这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。 Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值