synchronized的底层实现原理
实现synchronoized的基础
- java对象头
- Monitor
hotspot环境下对象在内存中的布局
主要分为三块区域:
- 对象头
- 实例数据
- 对齐填充
对象头
synchronized使用的锁对象都是存储在对象头里的.
对象头的结构
由图可知:
- Class Metadata Address是对象指向其的类元数据的指针,JVM通过这个指针来确定该对象是哪个类的实例.
- Mark Word用于存储对象自身的运行时数据,这是重点,对于轻量级锁和偏向锁有关键性的作用.
Mark Word
对象头的信息是与对象自身定义的数据没有关系的额外存储成本
,考虑到jvm的空间效率,Mark Word被设计成一个非固定的数据结构以便存储更多有效的数据.
其中的重量级锁便是synchronized的对象锁.
Monitor
每个java对象天生自带一把看不见的锁(Monitor锁,可以认为它就是一个锁对象)
结合上图:
Mark Word中指向重量级锁的指针 指向的便是Monitor锁对象的地址.每个对象都存在着一个monitor与之关联.
Monitor对象存在于每个java对象的对象头中
,synchronized便是通过这种方式去获取锁的.
这就说明为什么java对象都可以作为锁
Monitor锁的竞争,获取和释放
一个概念: 什么是重入:
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但 当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入
,重入请求是会成功的,即synchronized是具有可重入性的
.
因此,在一个线程调用synchronized方法的同时,在其方法体内部再去调用该锁对象的另一个synchronized方法是可以成功的.
为什么很多时候会对synchronized嗤之以鼻
- 在java早期版本中,synchronized属于重量级锁,效率很低,因为Monitor锁是依赖于底层操作系统的Mytex Lock实现的.
- 操作系统线程之间的切换需要从用户态转换到核心态,开销较大.
java6以后,synchronized性能得到了很大的提升.
下面这些技术都是为了实现线程之间更高效的共享数据以及解决竞争问题,从而程序的执行效率:
- 自适应自旋锁
- 锁消除
- 锁粗化
- 轻量级锁
- 偏向锁…等等
自旋锁与自适应自旋锁
自旋锁
产生原因: 在许多情况下,共享数据的锁定状态持续时间较短,切换线程划不来,可以让要获取该资源的另一个线程等一小会,但是不放弃CPU的执行时间
,这个等一会但不放弃CPU执行时间的行为就是自旋.
实现方法: 通过让线程执行忙循环(可以联想为while(true)这样子) 来等待锁的释放,但是期间不让出CPU(即不像sleep一样放弃CPU的执行时间).
本质上自旋锁和阻塞不一样,如果锁被占用的时间很短的话,那么自旋锁的效率会很高,但是如果锁被其他线程占用的时间太长,会带来许多性能上的开销,因为线程自旋时始终会占用CPU的时时间片,自旋时间太长线程会白白浪费CPU的资源,因此自旋的等待时间应该有一定的限度,如果自旋超过了一定尝试次数,那就应该把这个线程扔到锁池里去
,用户可以通过PreBolckSpin这个参数来修改自旋次数.
由于每次线程使用锁的时间和等待的时间是不固定的,所以自旋锁的次数很难设计的比较合理,那咋整啊?
用更聪明的锁—自适应自旋锁
自适应自旋锁
- 从java6开始引进.
- 自适应自旋锁的
自旋次数不再固定
,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定.
如果在同一个锁对象上刚刚自旋成功获取锁的线程正在运行中,那么jvm就会认为该锁通过自旋获取锁的可能性很大,那么就会自动增加等待时机(即自旋次数,例如增加到50次
).
相反的,如果对于某个锁,自旋很少成功获取到锁,那么之后在获取这个锁的时候就会减少等待时间甚至不自旋了,直接扔锁池里面去
.
这样子之后,jvm对锁的自旋次数的预测就会越来越精准.
锁消除
它是对锁进行优化的另一种方式,这种优化更彻底.
- 它会在JIT编译时,对运行的上下文进行扫描,取出不可能竞争的锁.
锁消除例子:
JIT编译指的时即时编译,如图所示:
锁粗化
一般我们都是通过希望缩小锁的范围来加快执行速度,但是当有重复操作的时候,不断的加锁释放锁会减缓程序的执行效率
,所以就有了锁粗化这个概念
- 通过扩大加锁的范围,避免反复加锁和解锁.
例子:
上述例子中append方法的反复执行,由于StringBuffer的append方法是synchronized修饰的,所以是线程安全的,反复执行append,不断的加锁释放锁会使得执行效率较低,所以jvm采用锁粗化方式来讲锁的范围放到循环执行的整个过程.