首先了解常见的锁概念:
互斥锁/独占锁、共享锁
乐观锁、悲观锁
自旋锁、可重入锁、不可重入锁
轻量级锁、重量级锁、偏向锁。
现在回忆一下同步关键字synchronized的使用场景:
1.用于实例方法、静态方法时,隐式指定锁对象;实例方法隐式锁定this对象,静态方法隐式锁定当前类的class对象;
2.用于代码块时,显示指定锁对象。
其特性可以概括为:可重入、独占、悲观。
接下来看一段java代码:
synchronized(this){
i++;
}
那么思考一下,这个synchronized是怎么让这this记住是当前这个线程来加的锁,而不是别的线程呢?
要解答这个问题,就得先要了解一下java对象在内存中的存储结构:
Java对象在内存中的存储结构分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
synchronized依靠对象头的mark word来记录锁相关信息的,mark word用途解释:
1)用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits;
2)Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
下面来进一步看看mark word的存储结构:
无锁状态下的存储结构:
加锁状态下的存储结构:
了解了mark word的存储结构之后,开始java中加锁的过程,首先看一张锁的升级过程图:
其中,轻量级锁和偏向锁是Java6对synchronized优化后新增加的。
下面分析图中2条加锁路线,先看红色锁升级路线:
假定关闭偏向锁的优化,线程A开始进入synchronized代码处,开始抢锁,如果抢锁成功,则Lock record address记录为当前线程A,此时的锁称为轻量级锁;此时线程B也进入synchronized代码处,开始抢锁,则进行CAS自旋抢锁,在自旋到一定次数后,则发生锁升级--升级为重量级锁。重量级锁则需要借助另一个对象实现--即monitor对象(在Java中,每个对象都有一个与之关联的monitor对象)。同时,mark word中的minitor address即指向与之关联的monitor对象,线程B进入monitor的锁池(EntrySet)中。
![](https://img-blog.csdnimg.cn/20191008173630909.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RlcHQxMjM=,size_16,color_FFFFFF,t_70)
当线程A退出synchronized代码块后,线程B则会被从EntrySet中转到WaitSet中,等待获取锁,当线程B获取到锁后,执行线程B的同步代码块。
如果开启了偏向锁优化(图中蓝色执行路径),线程A获取偏向锁后,当线程A再次执行同步代码块,就不用在抢锁,直接获取到锁,进入同步代码执行;此时线程B开始抢锁,则出现争抢,发生锁升级,在自旋到一定次数后,升级为重量级锁。后面的执行逻辑与之前的一样,不再赘述。
文章参考:
网易云课堂课程
文章参考链接:
https://www.cnblogs.com/monkey0307/p/9667606.html