文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
锁优化
高效并发是从JDK 5升级到JDK 6后一项重要的改进项
HotSpot
虚拟机开发团队在这个版本上花费了大量的资源去实现各种锁优化技术
如适应性自旋、锁消除、锁膨胀、轻量级锁、偏向锁等
这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率
自旋锁:
前面提到互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入 内核态 中完成
同时,在许多应用上,共享数据的锁定状态 只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得
因此,我们可以让没有请求到锁的线程等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快释放锁。而这个等待的操作
- 我们让线程执行忙循环(自旋)来实现,这就是所谓的自旋锁
[注]:在JDK 6中就已经改为默认开启,
-XX:+UseSpinning
控制自旋锁的性能分析:
自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间:
- 如果锁被占用的时间很短,自旋等待的效果就会非常好
如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源
- 而不会做任何有价值的工作,这就会带来性能的浪费
因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁
- 就应当使用传统的方式去挂起线程
- 自旋次数的默认值是十次,用户也可以使用参数
-XX:PreBlockSpin
来自行更改
自适应自旋锁(自旋锁的优化):
无论是默认值还是用户指定的自旋次数,对整个Java虚拟机中所有的锁来说都是相同的
而在JDK 6中对自旋锁的优化,引入了自适应的自旋
自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功
- 进而允许自旋等待持续相对更长的时间,比如持续100次忙循环
另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源
锁消除:
锁消除是指虚拟机 即时编译器(
JIT
) 在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除
- (锁消除的主要判定依据来源于 逃逸分析的数据支持))
如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待
- 认为它们是线程私有的,同步加锁自然就无须再进行
锁粗化:
我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步
这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁
大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的
- 那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗
- 因为这时我们需要适当扩大同步块的作用范围
- 比如:在for循环中的同步块放到for循环外
轻量级锁:
轻量级锁是
JDK 6
时加入的新型锁机制,它名字中的轻量级是 相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为重量级锁设计的初衷:
- 不是用来替代重量级锁,而是在没有锁竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
HotSpot虚拟机的对象头分为两部分:
第一部分:用于存储对象自身的运行时数据,如哈希码、GC分代年龄等
- 这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,称为 Mark Word
- 这部分是实现 轻量级锁和偏向锁 的关键
另外一部分:
- 用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度
[注]:由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率
Mark Word
被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息
偏向锁:
偏向锁也是JDK 6中引入的一项锁优化措施
- 它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量
- 那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了
偏:
- 锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取
- 则持有偏向锁的线程将永远不需要再进行同步
启用参数:
-XX:+UseBiased Locking
,JDK 6起HotSpot虚拟机的默认开启)当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为01、把偏向模式设置为1,表示进入偏向模式
同时 使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中
- 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束
- 根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为0)
- 撤销后标志位恢复到未锁定(标志位为01)或轻量级锁定(标志位为00)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。
当对象进入偏向状态的时候,
Mark Word
大部分的空间(23个比特)都用于存储持有锁的线程ID了
- 这部分空间占用了原有存储对象哈希码的位置,那原来对象的哈希码怎么办呢?
首先在Java语言里面一个对象如果计算过哈希码,就应该一直保持该值不变(强烈推荐但不强制,因为用户可以重载hashCode()方法按自己的意愿返回哈希码)
而作为绝大多数对象哈希码来源
Object::hashCode()
方法,返回的是对象的一致性哈希码,这个值是能强制保证不变的
- 它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变
因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了
- 而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁
在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的
ObjectMonitor
类里有字段可以记录非加锁状态(标志位为01)下的Mark Word
- 其中自然可以存储原来的哈希码