聊聊Synchronized的优化

系统
在早期版本中,synchronized是一种重量级锁,其底层由Monitor实现,而Monitor又依赖于操作系统的Mutex Lock。线程获取到锁后,需要切换状态,而操作系统在实现线程的切换时,需要从用户态转为核心态,这是一个非常耗时,非常重的操作。

[[375798]]

 早期版本synchronized性能较低的原因

在早期版本中,synchronized是一种重量级锁,其底层由Monitor实现,而Monitor又依赖于操作系统的Mutex Lock。线程获取到锁后,需要切换状态,而操作系统在实现线程的切换时,需要从用户态转为核心态,这是一个非常耗时,非常重的操作。因此在之前,synchronized是一种重量级锁。

JDK1.6之后对synchronized的优化

现在的synchronized已经没有之前那么笨重了,在虚拟机层面,对synchronized做了较大的优化,引入了自旋锁、适应性自旋锁、锁消除、锁粗化,可以减少锁操作的开销。

自旋锁

有时候,获取到锁的线程执行的操作耗时极短,为了这么点微不足道的时间,将接下来等待锁的线程挂起非常的不值得。挂起线程的操作需要在核心态完成,从用户态切换到核心态,耗时比较严重。

因此现在增加这么一样操作,让等待锁的线程执行忙循环等待,不停地去尝试获取锁,像一种自旋的操作,故称之为自旋锁。

如果之前线程占有锁的时间极短,那么自旋锁的性能将非常的好。但若是占有锁的时间较长,那么自旋锁将白白消耗CPU的资源,在自旋次数到了之后,将会被挂起。

在jdk1.4的时候,自旋锁默认关闭;jdk1.6之后,自旋锁默认开启,默认自旋10次,当然也可以使用PreBlockSpin来修改自旋次数。

自旋锁的痛点在于:无法在不同场景中,确定出一个可靠的自旋次数。因此,衍生出来适应性自旋锁。

适应性自旋锁

在适应性自旋锁中,自旋的次数不再固定,一般由之前自旋的次数和锁持有者的状态决定。

如果在一个锁对象上,之前的线程都能通过自旋来获取到锁,并且没有超过自旋次数,那么虚拟机认为,通过自旋获取到锁的概率很大,下一次会增加自旋的次数。相反的,如果之前很少有线程通过自旋获取到锁,那么虚拟机会减少自旋的次数,减少到一定次数后,甚至会直接放弃自旋,升级为重量级锁。

可以看出,适应性自旋锁十分机智。

锁消除

从字面意思上可以看出,这是一种直接去除锁的方法,简单粗暴。

对于那些根本不可能存在锁竞争却又包含锁的情况,虚拟机会直接消除这个锁,避免无意义的锁请求。比如我在纯单线程中对某个方法或者变量加锁,或者调用内部实现有锁的对象(Vector、StringBuffer与HashTable等),虚拟机会直接消除毫无意义的加锁。

锁粗化

在上一文中【多线程】浅说Synchronized,我们谈到了synchronized的应用-双重检验锁的优化过程,强调将加锁的范围尽量限制得小一些,直到存在锁竞争的实际区域才加锁,这样程序运行更加高效。

但是,如果存在这样的一种情况:反复的对同一个对象执行加锁解锁的操作,也会导致CPU资源的过度消耗。

锁粗化,就是将反复的加锁操作粗化成一个范围更大的锁,这样加锁只有一次。

例如,在循环内部,调用StringBuffer的append操作(关于StringBuffer,可以参考我的另外一篇文章【JAVA】String、StringBuilder、StringBuffer三者的区别),每次append都需要加锁,虚拟机检测到这种情况后,首先会对append脱锁,然后进行锁粗化,将锁的范围扩大到循环外部。

锁的状态

锁的状态有以下几种:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

其中,无锁状态对应于锁消除,Monitor对应于重量级锁,也就是1.6之前的synchronized。

偏向锁

偏向锁的核心要义就体现在“偏”字上,这个锁偏向第一个获取到它的线程。

在大部分情况下,不存在激烈的锁竞争,总是由同一个线程获取到该锁。那么为了减少同一个线程获取锁带来的开销,就引入了偏向锁。

如果一个线程不断的获取到了锁,那么该锁就进入偏向锁状态。当这个线程再次请求锁时,无需做任何同步操作,直接获取到锁。

当然,偏向锁适用于基本无锁竞争的情况,当锁竞争激烈时,偏向锁就失去了作用,会升级为轻量级锁。

轻量级锁

在偏向锁的状态下,此时又出现了一个线程,与偏向线程竞争该锁,此时该锁会升级为轻量级锁。

举个例子,比如创建一个线程1执行同步print()方法打印奇数,这时候的锁状态为偏向锁。此时,再创建一个线程2同样执行同步print()方法打印偶数,偏向锁就会升级为轻量级锁。线程1打印某个奇数时,线程2并没有被挂起,而是处于一种自旋状态,这种自旋效率很高。可是,当我再创建100个线程时,同样执行同步print()方法,自旋的效率将会变得十分低下,此时轻量级锁会升级为重量级锁,即使用Monitor来进行同步。

锁的升级

无锁、偏向锁、轻量级锁与重量级锁,会随着锁竞争的升级而升级。

从一开始的偏向锁,产生锁竞争后,升级为轻量级锁,自旋失败后,升级为重量级锁,一般来说,锁的升级是单向的。

 

责任编辑:姜华 来源: 今日头条
相关推荐

2022-07-04 08:01:01

锁优化Java虚拟机

2023-09-01 08:59:57

2021-06-11 06:54:35

DPDK优化HugeTLB

2024-02-29 18:06:39

HTTP性能优化

2020-12-31 05:33:34

软件性能优化

2021-08-03 07:40:46

Synchronize锁膨胀性能

2020-11-12 08:32:14

Vue3模板优化

2021-11-10 18:52:42

SQL技巧优化

2021-08-10 08:01:08

Synchronize锁膨胀锁消除

2023-07-13 11:24:14

SQL优化赋值

2023-07-31 07:48:43

Java内存虚拟机

2023-05-26 18:52:55

2020-11-24 11:16:06

JavaScript

2022-03-11 10:23:02

React性能优化

2021-11-18 08:20:22

接口索引SQL

2021-09-03 23:01:58

CSS 技巧代码重构

2023-12-02 20:41:32

内存kube

2023-03-26 22:42:02

SQL关联索引

2022-02-21 13:27:11

接口性能优化索引命令

2022-07-20 08:21:00

Java代码优化
点赞
收藏

51CTO技术栈公众号