synchronized锁升级优化
在JDK1.6以前,使用synchronized就只有一种方式即重量级锁,而在JDK1.6以后,引入了偏向锁,轻量级锁,重量级锁,来减少竞争带来的上下文切换。
Java对象头
每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成
Mark World 记录了对象和锁有关的信息,在64位JVM中Mark World的长度是64bit 具体结构如下
锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。
一般的synchronized同步锁升级步骤是:
偏向锁 -> 轻量级锁 -> 重量级锁
用户态和内核态
- 内核态
CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU也可以将自己从一个程序切换到另一个程序
- 用户态
只能受限的访问内存,切不允许访问外围设备。占用CPU的能力被剥夺,CPU资源可以被其他程序获取。
之所以会有这样的却分是为了防止用户进程获取别的程序的内存数据,,或者获取外围设备的数据。
什么时候进行切换
用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核的事情,例如从硬盘读取数据,或者从硬盘获取输入,而唯一可以做这些事情的就是操作系统(synchronized中依赖的monitor也需要依赖操作系统完成,因此需要用户态到内核态的切换)所以程序就需要先操作系统请求以程序的名义来执行这些操作。
这时候就需要:将用户态程序切换到内核态,但是不能控制在内核态中执行的命令 这部分先不做太多解释,需要知道的是synchronized是依赖操作系统实现的,因此在使用synchronized同步锁的时候需要进行用户态到内核态的切换。
https://www.cnblogs.com/shangxiaofei/p/5567776.html
synchronized 内核态切换
简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。
优化后的synchronized锁
因为线程切换是非常重量级的因此 JDK1.6开始对synchronized做了优化,通过上文讲的Mark World 来区分了不同场景下同步锁的不同类型,来减少线程切换的次数.
偏向锁
偏向锁的作用是当有线程访问同步代码或方法时,线程只需要判断对象头的Mark Word中判断一下是否有偏向锁指向线程ID.
偏向锁记录过程
线程抢到了对象的同步锁(锁标志为01参考上图即无其他线程占用)
对象Mark World 将是否偏向标志位设置为1
记录抢到锁的线程ID
进入偏向状态
偏向锁的优势
通过加偏向锁的方式可以看到,对象中记录了获取到对象锁的线程ID,这就意味如果短时间同一个线程再次访问这个加锁的同步代码或方法时,该线程只需要对对象头Mark Word中去判断一下是否有偏向锁指向它的ID,不需要在进入Monitor去竞争对象了。
什么时候升级成轻量级锁?
偏向锁 -> 轻量级锁 -> 重量级锁
一旦出现其他线程竞争资源时,偏向锁就会被撤销。
偏向锁的插销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁。反之则其他线程抢占。
即如果线程在全局安全点检查时,还需要使用该锁 则进行锁升级,如果线程已经不需要使用锁,并有其他线程需要使用时,将偏向锁的拥有者切换为另外线程。
轻量级锁
当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
- 举个例子来说明一下什么时候需要升级偏向锁
假设A线程 持有锁 X(此时X是偏向锁) 这是有个B线程也同样用到了锁X,而B线程在检查锁对象的Mark World时发现偏向锁的线程ID已经指向了线程A。这时候就需要升级锁X为轻量级锁。轻量级锁意味着标示该资源现在处于竞争状态。
当有其他线程想访问加了轻量级锁的资源时,会使用自旋锁优化,来进行资源访问。
自旋策略
JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。
从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。
重量级锁
自旋失败,很大概率 再一次自选也是失败,因此直接升级成重量级锁,进行线程阻塞,减少cpu消耗。
当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。
总结
synchronized锁升级实际上是把本来的悲观锁变成了 在一定条件下 使用无所(同样线程获取相同资源的偏向锁),以及使用乐观(自旋锁 cas)和一定条件下悲观(重量级锁)的形式。
偏向锁:适用于单线程适用锁的情况
轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似)
重量级锁:适用于竞争激烈的情况