Synchronized入门到踹门

知识铺垫

1、CAS

CAS全称叫做CompareAndSwap,有的也叫CompareAndSet、CompareAndExchange;意为比较并交换。
CompareAndSwap是Unsafe类中的一个方法,主要参数有三个:
主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
工作内存中共享变量的副本值,也叫预期值:A
需要将共享变量更新到的最新值:B

主内存中保存了一个共享变量,线程通过这个V来获取到这个共享变量到这个线程的工作内存A中,然后经过计算后变成B值,最后再将B值写回到内存V值中去。
CAS的核心就是在将B值写入回去的时候,通过V值获取到主内存中的共享变量与A值对比是否相同,如果相同则将B写入到主内存中,如果不相同,就说明在此期间有其他线程修改了主内存中的值,则继续重复上述操作。
在这里插入图片描述

CAS的具体实现是native的方法,native指的是原生函数,是用C或C++实现的方法。

CAS被称为无锁操作,实际上真的是无所操作吗?
如果CAS是无锁操作,那么就会有一个疑问,因为比较A与V值和将B值写入主内存一定是两步操作,那么在如果线程A比对完两个值发现相同,可以将B写入到主内存中,此时线程B也比对完两个值,然后将自己线程计算完成的B值写入到主内存中,此时A线程又将自己计算完成的B值写入到主内存中,这个时候CAS操作就不是一致性的,所以我们再想CAS真的没有加锁吗?
实际上如果一步步去追踪源码,会发现它最终调用了汇编写的LOCK_IF_MP方法,意思是在多个处理器的情况下就加上LOCK指令,如果是单个处理器的情况下就不加LOCK指令。
最终执行的是lock cmpxchg指令,lock指令不允许被其他CPU打断,保证执行的一定是原子操作,所以说实际上CAS最终可能还是加了锁,并不是无锁操作。

提到CAS就必须要说CAS的ABA问题:
A线程通过V获取到主内存中的共享变量为1,在自己工作内存中将1变更为2。
B线程通过V获取到主内存中的共享变量为1,在自己工作内存中将1变更为2,对比V与A相同,将2写回主内存。
C线程通过V获取到主内存中的共享变量为2,在自己工作内存中将2变更为1,对比V与A相同,将1写回主内存。
A线程对比V与A相同,将2写回主内存。

此时就出现了问题,A线程虽然比对相同,但是在中间过程中已经有其他线程修改过这个值,所以A线程的这一次CAS操作严格来说并不是原子性的操作。

JDK为了避免ABA问题的发生,增加了一个AtomicStampedReference类,在CAS原有的基础上增加了一个参数S版本号/标记,每次主内存中的共享变量被修改过一次,版本号就会发生一次变化,比对的时候首先检查V与A值是否相等,然后再检查当前版本号是否与预期版本号相同,都相同才会将B值写回主内存,否则不修改,重新来过。

在这里插入图片描述

JUC包下的Atomic*类都是通过CAS来实现原子性的操作的。

2、对象的内存布局

这里我们讲的只是HotSpot实现的虚拟机的对象内存布局,我们可以将其划分为三个部分:对象头、实例数据、对其填充。

在这里插入图片描述

对象头:
对象头包含两类信息:
1、存储对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据也被官方称之为“Mark Word”。
2、类型指针,即对象指向它的类型元数据的指针,也就是Java虚拟机通过这个指针来确定该对象是那个类的实例。但是并不是所有的对象都是这样的,比如如果是一个Java数据,那么对象头中还必须有一块专门用记录数组长度的数据。

实例数据:
这部分数据即我们在程序代码中定义的各种类型的字段内容,不论是从父类继承来的,还是在子类中定义的定义的字段都必须记录起来。

对齐填充:
这一部分并不是必然存在的,也没有什么特别的含义,它仅仅是起到占位符的作用,因为HotSpot实现的虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,对象头被精心设计过,正好是8字节的整数倍(可能是1倍也可能是2倍),实例数据不一定是8字节的整数倍,所以最后差多少字节,就需要这部分来填充补全。

正文

在这里插入图片描述

了解完对象的内存布局,我们会知道实际上Synchronized就是通过修改Mark Word来实现加锁解锁。
在这里插入图片描述

**偏向锁:**将第一次来获取该锁的线程ID或者说是线程指针、唯一标识贴在Mark Word当中,每次该线程获取来获取锁的时候,只要Mark Word还存在该线程的唯一标识就可以直接获取到锁。

**轻量级锁(自旋锁):**线程在自己的线程栈中生成一个LR(Lock Record),然后通过CAS的方式去修改Mark Word的指针,修改成功后,Mark Word的指针指向的就是这个线程的LR。

**重量级锁:**利用内核中的互斥量mutex,它是一个同步原语,它只允许对一个线程的共享资源进行独占访问,如果一个线程获取到了mutex,那么其他线程就无法获取到mutex。

偏向锁和轻量级锁都是在用户空间就可以完成的,不需要去内核申请mutex,所以这两个都被统称为轻量级锁,而重量级锁需要到内核态去申请mutex,中间还涉及了用户态与内核态之间的切换,很耗费资源、时间,而且最初Synchronized没有锁升级,每次获取都是重量级锁,但是大多情况下却只有一个线程在使用,正因如此,JDK对Synchronized进行了锁优化,引入了这两个轻量级的锁。

第一条线:

在这里插入图片描述

我们平时了解的可能更多的是无锁→偏向锁→轻量级锁→重量级锁,但是这条线直接略过了偏向锁。

想要搞明白为什么直接略过了偏向锁,可以先思考一个问题,当我们在明确知道会有锁争抢的前提下,偏向锁一定会比轻量级锁更快吗?

明确知道锁一定会被争抢的前提下,每个锁对象实际上都会从无锁升级成偏向锁,然后进行一步偏向锁锁撤销的过程,然后升级为轻量级锁,为了节省这一步锁撤销的过程,JVM在启动时直接关闭了偏向锁的开关,等待几秒钟过后在开启偏向锁,所以在未开启偏向锁的时候锁会直接升级为轻量级锁。

升级重量锁之前,线程都在自旋等待,那么升级为重量级锁之后,所有线程都会丢到一个叫WaitSet的队列中去,这个WaitSet是ObjectMonitor对象的一个属性,进入到这个队列中,这些线程就不需要自旋消耗CPU资源了,也就是为什么有了轻量级锁为什么还要使用重量级锁,这个时候进入这个队列等待,要比自己空转自旋要更合适。

我们再看字节码文件的时候可以发现:
在这里插入图片描述

monitorenter 上锁
monitorexit 解锁
但是你会发现生成了两条monitorexit指令:
第一条monitorexit是正常释放锁
第二条monitorexit是当抛出任何异常的时候也释放锁
所以说Synchronized是自动上锁,自动释放锁。

第二条线:

在这里插入图片描述

这一条线出现了匿名偏向的一个概念,意思是没有存放偏向的线程的标识,但是通过Mark Word会发现此时它是偏向锁。

第三条线:

在这里插入图片描述

在偏向锁的时候调用wait方法直接掠过轻量级锁,升级为重量级锁。

wait方法是Object类的方法,具体实现中调用完wait函数之后,Synchronized直接膨胀为重量级锁。
可重入性
Synchronized是可重入锁,A线程获取到了这把锁,可以在这个同步代码块中继续获取这把锁,但相应的最终释放锁的时候也要释放两次。

Synchronized是如何记录重入次数的呢?

偏向锁重入:
上面我们讲到了LR(Lock Record),线程再自己的线程栈中生产一个LR,Mark Word中记录了指向这个LR的指针,这个指针占用了Mark Word存放hashCode(identityHashCode:根据对象实际存储的物理地址生成的HashCode,和我们调用的HashCode值不是一个)的位置,hashCode被存到到了另一个地方,LR存放了可以寻找到这个地方的指针;
当线程再一次获取锁的时候,当前线程栈会再生成一个空的LR,每重入一次就会生成一个新的LR,每释放一次就会从栈中弹出一个LR,这就可以保证获取锁的次数和释放锁的次数一致。

轻量级锁重入:
轻量级锁的重入和偏向锁类似,通过生成空的LR来解决重入的问题。

重量级锁重入:
重量级锁Mark Word中存放的就不再是线程ID或者线程指针了,而是一个指向OBjectMonitor对象的指针,这是一个C++的对象。OBjectMonitor里有个属性记录了重入的次数。

竞争激烈:
有线程自旋超过10次(1.6之前可以通过-XX:PreBlockSpin修改),或者自旋线程数超过CPU核数一半,就会升级为重量级锁,1.6之后加入了自适应自旋,具体自旋多少次由JVM自己来控制,不需要自己修改。

以上测试全部是基于JDK1.8进行的,目前JDK11默认就是开启偏向锁的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ming Log

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值