基本特点
结合之前总结的锁策略,我们就可以总结出,synchronized具有以下特性(jdk1.8):
1.开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.
2.开始是轻量级实现,如果锁被持有的时间较长,就转换为重量级锁.
3.实现轻量级锁的时候大概率用到自旋锁策略
4.是一种不公平锁
5.是一种可重入锁
6.不是读写锁.
加锁加工过程
JVM将synchronized锁分为无锁,偏向锁,轻量级锁,重量级锁状态.会根据情况,依次进行升级.
注意基本点:锁只能升级,不能降级,非必要不加锁.
偏向锁
第一个尝试加锁的线程,优先进入偏向锁状态.
偏向锁并不是真的"加锁",只是给对象头做一个"偏向锁的标记",记录这个锁属于哪个线程.如果后续没有其它线程来竞争该锁,那么就不用进行其他同步操作了(避免了加锁解锁的开销).如果后续有其它线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别当前申请锁的线程是不是之前记录的线程),那就取消原来偏向锁的状态,进入一般轻量级锁的状态,进入一般轻量级锁的状态.
偏向锁本质上相当于"延迟加锁",能不加锁就不加锁,尽量来避免不必要的加锁开销.
但是该做的标记还是得做的,否则无法区分何时需要真正加锁.
轻量级锁
随着其它线程进入竞争,偏向锁状态被消除,进入轻量级锁的状态(自适应的自旋锁).
此处的轻量级锁通过CAS实现(认识过程即可,CAS后面会讲)
1.通过CAS检查并更新一块内存(比如null->该线程引用)
2.如果更新成功,则认为加锁成功
3.如果更新失败,则认为锁被占用,继续自旋式地等待(并不放弃CPU).
自旋操作是一直让CPU空转,比较浪费CPU资源.
因此此处的自旋不会一直持续进行,而是达到了一定的时间/重试次数,就不再自旋了.
也就是所谓的"自适应".
于此同时synchronized内部也会统计当前锁对象上,有多少线程参与竞争,如果整体CPU消耗大,则会转为重量级锁.
重量级锁
如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁.
此处的重量级锁就是指用到内核提供的mutex.
1.执行加锁操作,先进入内核态
2.在内核态判定当前锁是否已经被占用
3.如果该锁没有占用,则加锁成功,并切换回用户态.
4.如果该锁被占用,则加锁失败.此时线程进入锁的等待队列,挂起.等待被操作系统唤醒.
5.经历了一系列的沧海桑田,这个锁已经被线程释放了,操作系统也想起了这个挂起的线程,于是唤醒了这个线程,尝试重新获取到锁.
其它的优化操作
锁消除
也是synchronized中内置的优化策略,代码不需要加锁就会删除.
编译器+JVM判断锁是否可以消除,如果可以,就直接消除.
什么是"锁消除"
编译器优化的一种方式,编译器编译代码时,如发现该代码不需要加锁,就会把锁自动干掉.
有些应用程序的代码中,用到了synchronized,但其实没有在多线程环境下.(例如StringBuffer)
StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c");
此时每个append的调用都会涉及到加锁和解锁.但如果只是在单线程中执行这个代码,那么这些加锁解锁是没必要的,白白浪费了一些资源开销.
锁粗化
一段逻辑中如果多次出现加锁解锁,编译器+JVM会自动进行锁的粗化.
即讲多个细粒度的锁,合并成一个粗粒度的锁.(代码越少越细,越多越粗).
锁的粒度:粗和细
通常情况下,其实是更偏好让锁的粒度更细一些,更有利于多个线程并发执行的.
但是实际上可能并没有其它线程来抢占这个锁.这种情况JVM就会自动把锁粗化,避免频繁申请释放锁.
举个例子理解锁粗化:
领导给下属交代工作两种方式:
方式1:
打电话,交代任务1,挂电话.
打电话,交代任务2,挂电话.
打电话,交代任务3,挂电话.
方式2:
打电话,交代任务1,交代任务2,交代任务3,挂电话.
显然方式2更为高效.
相关面试题
什么是偏向锁?
偏向锁并不是真的加锁,而只是在锁的对象头中记录一个标记(记录该锁所属的线程).如果没有其它线程参与竞争锁,那么就不会真正执行加锁操作,从而降低程序开销.一旦真的涉及到其它的线程竞争,再取消偏向锁的状态,进入轻量级锁的状态.