emm…我更愿意把这篇博文当作是一篇笔记,其中有蛮多的东西是摘抄书本的,做一个思路上的整理。个人理解的部分其实并不太多,有错的地方欢迎评论区指出,还请大家多多包涵~
作用
被synchronized关键字所修饰的代码,在同一个时间段内,只允许一个线程访问其资源,保证了代码的同步性。
synchronized最终是依靠操作系统中的Mutex Lock实现的,Java是内核线程,也就是每一个Java线程会映射到操作系统中的一个线程,线程上下文的切换需要操作系统由用户态转换到内核态,比较消耗资源,所以早期synchronized关键字是性能比较低的。但是在JDK1.6 后,synchronized关键字有了许多优化,例如引进了偏向锁,轻量级锁等提升了性能。
实现
synchronized关键字的实现是JVM层面的,具体来说是在Java对象头的Mark Word中。
Java的对象头在32位虚拟机中为32字节,默认的结构如下:
锁升级
synchronzied经过优化后,其锁共有4种状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
每一种锁对应的对象头也不同
锁只可以升级,不能降级!
无锁
其对象头如表2-3所示
偏向锁
对象头:
其中:
- 线程ID:23字节
- Epoch:2字节
- 对象分代年龄:6字节
- 偏向锁标记(即表格中的1):1字节
- 锁标志位:2字节
注:这个Epoch与批量重偏向有关
为什么会有偏向锁
这里的“偏向”就是偏袒的意思,偏袒谁?偏袒曾经对某一个资源获取到锁的线程。因为在实践过程中,JVM的作者发现锁不仅存在多线程竞争,而且总是由同一线程多次获得。为了减少同一线程重复获得同一个锁的代价,因此引入了偏向锁。
偏向锁的加锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储这个线程的ID,以后该线程在进入和退出同步块的时候就只需要简单测试一下对象头Mark Word里是否存储指向当前线程的偏向锁就可以了。
如果测试成功,表示成功获得锁;如果测试失败,则需要再测试一下Mark Word中偏向锁标志位的值是否位1(表示获得了偏向锁),如果没有设置,使用CAS竞争锁,如果设置了,尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的释放
在偏向锁加锁的过程中,如果是同一个线程重复获得锁,那么就会在当前线程的栈帧中放入一条锁记录,在该线程退出同步块的时候,将这条锁记录弹出。
偏向锁的撤销
偏向锁的撤销与释放是不同的,撤销是指在偏向锁获得的过程中出现竞争才发生的。
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置为无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向锁对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记为不适合作为偏向锁,最后唤醒暂停的线程。
下图展示了偏向锁的加锁-释放过程
轻量级锁
对象头
其中:
- 指向栈中锁记录的指针:30字节
- 锁标志位:2字节
轻量级锁的加锁
- JVM在当前线程的栈帧中创建用于存储锁记录的空间
- 将对象头中的Mark Word复制到锁记录中
- 线程尝试使用CAS原子操作将对象头中的Mark Word替换为指向栈帧中锁记录的指针
- 替换成功:成功获得锁
- 替换失败:说明存在竞争,采用自旋的方式尝试获得锁
轻量级锁的解锁
- 当前线程使用CAS操作将存储在栈帧锁记录中的Mark Word替换回对象头
- 替换成功:成功解锁
- 替换失败:存在锁竞争,轻量级锁会膨胀成重量级锁
下图展示了轻量级锁的加锁-升级为重量级锁的过程
重量级锁
对象头
其中
- 指向互斥量(重量级锁)的指针:30字节
- 锁标志位:2字节
概述
当锁处于重量级锁时,其他线程对于这个锁的请求都会被阻塞,只有当持有锁的线程释放锁之后,才开始新一轮锁竞争。
个人感觉:重量级锁于其他情况下的锁不一样的地方就是阻塞,其他情况下,竞争线程都在不断地进行尝试,并没有阻塞。
作用范围
-
同步代码块
使用字节码指令:moniterexter和moniterexit实现,被这两个字节码指令包裹的部分即为同步部分。
-
同步方法
使用方法标志位:ACC_SYNCHRONIZED实现
当synchronized关键字作用于同步代码块时,它既可以锁对象,也可以锁类(静态资源),它们不冲突,也就是线程A可以锁某一个对象的实例,线程B同时可以锁整个类(静态资源)。
参考资料:
- 《Java并发编程的艺术》
- 百度百家号