JVM之锁的优化

来源:互联网 发布:房地产横盘 知乎 编辑:程序博客网 时间:2024/05/04 05:33

  • 自旋锁Spinning
  • 锁消除
  • 锁粗化
  • 轻量级锁
  • 偏向锁
  • 对象内存布局

JDK 1.5 到 JDK 1.6的一个重要改进,便是高效并发。此时实现了各种锁优化技术,为了高效地在线程之间共享数据,解决竞争问题,从而提高执行效率。

  • 适应性自旋锁(Adaptive Spinning)
  • 锁消除(Lock Elimination)
  • 锁粗化(Locking Coarsening)
  • 轻量级锁(Lightweight Locking)
  • 偏向锁(Biased Locking)

1. 自旋锁(Spinning)

默认开启-XX:PreBlockSpin 修改自旋次数,默认10

有些共享数据的锁定状态,只会持续很短的时间,但互斥同步的阻塞实现,需要转入OS的内核态,带来并发性能瓶颈。所以,要为这么的短时间而切换内核态吗?

自旋原理
如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需让线程执行一个忙循环(自旋)

自旋缺点

  • 自旋等待本身虽然避免了线程切换的开销,但它要占用处理器时间。所以如果锁被占用的时间很短,自旋等待的效果就非常好
  • 如果时间很长,那么自旋的线程只会白白消耗处理器的资源。所以自旋等待的时间要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,那就应该使用传统的方式挂起线程了。

JDK 1.6引入自适应的自旋锁,为的,是解决自旋等待时间问题。

自适应,意味着,自旋时间由前一次在同一个锁上的自旋时间及该锁的拥有者的状态来决定。

  • 在同一个锁对象上,自旋等待刚刚成功获得过锁,而且,持有这个锁的线程正在运行,那么,JVM就认为这次自旋,也很有可能成功,进而允许自旋等待相对长些。
  • 如果对于某个锁,自旋很少成功,那在以后要获取这个锁,可能省略掉自旋过程,以免浪费处理器资源。

这就有点机器学习的意味了。

2. 锁消除

这是JVM的JIT编译器做的编译优化。

锁消除
JIT运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁,会被消除。判定依据为逃逸分析

关于逃逸分析,请看JVM之执行引擎

3. 锁粗化

原则上,编程时,推荐将锁的粒度限制得尽量小,只在共享数据的实际作用域中才做同步,使得同步操作数量尽可能变小。如果存在锁竞争,等待锁的线程也能尽快拿到锁。

但是,若是一系列连续操作都是对一个对象的反复加锁和解锁,此时,虚拟机探测到有这么一串操作,都是对同一个对象加锁,那加锁同步的范围,也就是锁的范围,会扩展到整个操作序列外部。

例如多个StringBuffer.append()连续对一个对象操作。

4. 轻量级锁

JDK1.6加入。

没有多线程竞争的前提下,减少传统的重量级锁,使用操作系统互斥量产生的性能消耗。关键在于,使用了非阻塞同步,CAS操作。

依据:绝大部分的锁,在整个同步周期内都不存在竞争—经验数据

加锁步骤

  • (1)对象头的Mark Word会被复制到当前线程的栈帧中,放到一个叫做锁记录(Lock Record)的空间,这份拷贝加了个Displaced前缀

  • (2) 虚拟机执行CAS操作,尝试将对象的Mark Word更新为一个指向Lock Record的指针

  • 如果更新操作成功,该线程获得对象的锁,对象的Mark Word的锁标志位更新为“00”

  • 如果更新操作失败,虚拟机先查看对象的Mark Word是否已经指向了当前线程的栈帧;若是,说明线程已经拥有该对象的锁,否则,说明对象被其他线程抢占了

lightweightlocking

5. 偏向锁

消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

即在无竞争的情况下,把整个同步都消除掉,连CAS都不做

这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

加锁步骤

  • (1) 当锁对象第一次被线程获取时,虚拟将对象头的标志位设为“01”
  • (2) 虚拟机执行CAS,将ThreadId记录到对象的Mark Word中。若成功,持有偏向锁的线程以后每次进入该锁相关的同步块时,都不进行任何同步操作
  • 当有另外一个线程尝试获取这个锁时,偏向模式结束。根据对象是否被锁定,执行后续操作

biasedlocking

-XX:+UseBiasedLocking

6. 对象内存布局

HotSpot虚拟机的对象内存布局.对象由3部分组成。

  • 对象头(Object Header)
    • 运行时数据(32bit or 64bit depending on OS)
    • HashCode
    • Generatiional GC Age
    • 类型指针 + 数组长度(如果是数组对象)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

以下是对象头里,运行时数据(又叫Mark Word)存储的内容表

存储内容 标志位 状态 对象HashCode,对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不记录信息 11 GC标记 偏向线程ID,偏向时间戳,对象分代年龄 01 可偏向
0 0