在前面并发容器的学习中我们多次发现代码之中使用了Atomic包下的类,以及synchorized关键字,今天我们就来扒一扒 synchronized 关键字。 synchronized 关键字使用的三种场景
1.对于普通同步方法,锁是当前实例对象
2.对于静态同步方法,锁是当前类的Class对象
3.对于同步方法块,锁是Synchronized括号里配置的对象
接下来我们来看看synchronized关键字是如何实现的我们从jvm虚拟机规范中找到其根据
从这段描述中我们知道java的实现方案有两种 一种针对方法是使用ACC_SYNCHORNIZED标志后由方法调用和返回指令实现,而对于代码块的实现使用的是monitorenter 和 monitorexit指令来实现。
而从上面的描述中我们也知道为什么以前称synchronized是重量级锁,因为其由jvm底层实现,每次加锁是所谓的由用户态到内核态的转换。而我们的synchronized是java的宝贝儿子,从1.6开始了一波又一波的优化。现在已经不能单纯的说其效率低,性能消耗大了。我们首先确认synchronized存在哪里。从官方文档中我们可以找到是存在对象头之中的Mrak Word
具体结构如下
从1.6版本为了降低锁的性能消耗,引入了“偏向锁” 和“轻量级”锁 。锁一共有四种状态,分别是,无状态锁,偏向锁,轻量级锁和重量级锁。下面来说说锁升级
偏向锁出现的原因是jvm的开发者经过研究发现,大多数情况下,锁不存在竞争,并且总是由同一线程反复获得。所以为了降低获得锁的代价引入了偏向锁,偏向锁一般不会自动释放。除非达成某种条件才会释放锁。
获得偏向锁后ThreadId将指向获取锁的线程ID,如果尝试获取偏向锁失败需要先判断偏向锁标示位是否为1,然后尝试使用cas操作将偏向锁指向当前线程ID。当出现锁竞争的时候会将偏向锁升级为轻量级锁
轻量级锁时,首先抢锁的线程会将当前 markword copy到自己的 线程栈的lock record中。接下来尝试使用cas操作将markword 指向当前线程 lock record的地址。如果成功则抢锁成功,如果失败则自旋抢锁。自旋操作虽然不阻塞当前线程,但是会损耗cpu资源。当自旋到达一定数量后,锁再次升级为重量级锁。(注意:锁只能升级不能降级)
当进入重量级锁时,我们就提到了monitor对象 ,让我们翻看一下openjdk中 monitor实现在objectMonitor.hpp中代码如下
上图为线程运行时示意,下面我将简述该过程。首先线程进入EntrySet,抢锁,首先线程会去判断count是否为0 ,尝试使用cas操作改变count值为1,如果失败则进入waitSet,如果成果则将owner 指向当前线程。 如果count不为0时,则需要判断owner是否为当前线程如果时则count+1重入。如果不是当前线程则失败。线程释放锁时,首先判断当前线程是否为owner,如果是则执行count -1 ,并判断 count是否为0 如果不是则释放锁失败。直至count为0时才释放成功。
当线程执行wait/notify操作时。如果线程wait,则标示释放锁,该线程进入WaitSet中等待。由于EntrySet 和 WaitSet 同时竞争锁,所以synchronized为非公平锁。
对于synchronized关键字还有锁粗化,锁消除等优化:
例如代码总是在带线程下使用,jvm则会对synchronized关键字进行消除。(锁消除)
例如 一个方法中中有多段synchronized关键字修饰的代码,jvm在不影响运行结果的前提下,会将部分代码移入synchronized修饰的代码块中,或将多个代码块合并。
今天的总结就到这里
欢迎大家指正问题 !! 与君共勉