文章目录
系列文章
1.前言
根据上一篇文章Java并发编程|第三篇:synchronized锁,其实发现,synchronized(lock)
的作用范围和lock
锁的对象有关
我们先来看看对象在内存中的内存布局
2.对象内存布局
- 对象头
第一部分:存储对象自身的运行时数据,如
哈希码
,GC分代年龄
,锁状态标志
,线程持有的锁
,偏向线程ID
,偏向时间戳
等。另一部分:类型指针,即对象指向它的元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例如果对象是一个数组,那在对象头中还必须有一块用于记录数组长度的数据。
- 实例数据
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
- 对齐填充
占位符作用,hotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,当实例数据部分没有对齐时。就需要通过对齐填充来补全。
发现对象的对象头中关键字锁状态标志
,线程持有的锁
,偏向线程ID
和锁有关系
2.1Mark Word
对象头中详细的分布
32位虚拟机中,不同锁状态,对象头存储的锁信息
3.锁优化
jdk1.6之前, 是基于重量级锁来实现的
既要保证数据安全,也要保证性能?
jdk1.6之后引入了锁升级
的概念
无锁–>偏向锁–>轻量级锁–>重量级锁
锁升级
的过程由jdk底层实现,并不需要用户干预,随着锁的升级,程序的性能逐渐降低
3.1场景
synchronized(lock){
//同步代码块
}
假设有线程1和线程2
-
只有Thread1访问同步代码块(大部分情况下)–>引入偏向锁
如果程序大多数时候不存在锁竞争,一个
同步块
一直只有一个线程访问,这种情况下引入了偏向锁的概念,减少获取锁的成本。 -
Thread1和Thread2交替访问同步代码块–>升级轻量级锁
在线程锁竞争不多,而且每个线程持有锁时间很短的情况下,通过让线程
自旋
的方式竞争锁,从而避免线程阻塞,CPU从用户态转到内核态代价比较大。 -
多个线程同时访问同步代码块–>膨胀为重量级锁
线程高并发,自旋超过限制,线程阻塞。
3.2无锁–>偏向锁
3.2.1线程1执行步骤:
-
线程1访问同步代码块
-
检查
lock
对象的对象头(mark word
)中是否存储了线程1 -
如果没有,则通过CAS替换,设置
mark word
中的线程ID
为T1
CAS替换 (原子性操作,乐观锁)
Compare and swap(value,expect,update)
通过现有值value和预期值expect比较
如果相同替换成功,表示一直是同一个线程1在访问
如果不同替换失败,表示有多个线程访问
4.执行同步代码块
3.2.2线程2执行步骤:
- 线程2访问同步代码块
- 检查
lock
对象的对象头,发现存储的是线程1,CAS替换失败 - 撤销线程1的偏向锁,重新恢复成无锁状态
3.3无锁->轻量级锁->重量级锁
3.3.1线程1执行步骤:
-
线程1访问同步代码块
-
通过CAS替换成功,设置
mark word
替换为轻量级锁 -
执行同步块
-
CAS替换失败(线程2争夺锁膨胀为重量级锁),线程1释放锁,并唤醒线程2
3.3.2线程2执行步骤:
-
线程2访问同步代码块
-
通过CAS替换失败,自旋(一段时间或者一定次数的自旋)
自旋
无限循环,直到cas返回true,即拿到锁,return跳出循环
for(;;){ if(cas){ return; } }
-
如果获取到锁,执行同步块
-
如果一直未获取到锁,在一段时间或者一定次数的自旋之后,锁膨胀升级为重量级锁
-
线程2阻塞
-
线程1执行完,释放锁,唤醒线程2
-
线程2重新争夺锁执行同步块
4.参考
腾讯课堂->咕泡学院->mic老师->并发编程基础
5.系列链接
上一篇:Java并发编程|第三篇:synchronized锁
下一篇:Java并发编程|第五篇:可见性,java内存模型