synchronized原理

目录

基本特点

加锁加工过程

偏向锁

轻量级锁

重量级锁

其它的优化操作

锁消除

锁粗化

相关面试题


基本特点

结合之前总结的锁策略,我们就可以总结出,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会自动进行锁的粗化.

即讲多个细粒度的锁,合并成一个粗粒度的锁.(代码越少越细,越多越粗).

锁的粒度:粗和细

synchronized原理_第1张图片

通常情况下,其实是更偏好让锁的粒度更细一些,更有利于多个线程并发执行的.

但是实际上可能并没有其它线程来抢占这个锁.这种情况JVM就会自动把锁粗化,避免频繁申请释放锁. 

举个例子理解锁粗化:

领导给下属交代工作两种方式:

方式1:

打电话,交代任务1,挂电话. 

打电话,交代任务2,挂电话. 

打电话,交代任务3,挂电话. 

方式2:

打电话,交代任务1,交代任务2,交代任务3,挂电话. 

显然方式2更为高效. 

相关面试题

什么是偏向锁?

偏向锁并不是真的加锁,而只是在锁的对象头中记录一个标记(记录该锁所属的线程).如果没有其它线程参与竞争锁,那么就不会真正执行加锁操作,从而降低程序开销.一旦真的涉及到其它的线程竞争,再取消偏向锁的状态,进入轻量级锁的状态. 

你可能感兴趣的:(java,开发语言)