人的知识就好比一个圆圈,圆圈里面是已知的,圆圈外面是未知的。你知道得越多,圆圈也就越大,你不知道的也就越多。

0%

深入理解Java虚拟机-线程安全与锁优化

线程安全

可以将 Java 语言中各种操作共享的数据分为以下五类:

  • 不可变
  • 绝对线程安全
  • 相对线程安全
  • 线程兼容
  • 线程对立

线程安全的实现方法

  • 互斥同步
  • 非阻塞同步
  • 无同步方案

锁优化

自旋锁与自适应自旋

现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器上有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋。

自旋等待不能替换阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但是它是要占用处理器的时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。

JDK6 引入了自适应的自旋,自适应意味着自旋的时间不再是固定的(10 此)了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在用一个锁对象上,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很可能再次成功,进而允许自选等待持续相对更长的时间,比如持续 100 次循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃出去被其他线程访问,那就可以把它们当作栈上的数据对待,认为它们是线程私有的,同步加锁自然就无需再进行。

锁粗化

如果虚拟机探测到存在零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

轻量级锁

轻量级锁名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。不过,需要强调一点,轻量级锁并不是用来替代重量级锁的,它设计的初衷是在没有多线陈竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

偏向锁

引入偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不去做了。

偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(TradeOff)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。

偏向锁无法使用自旋锁优化,因为一旦有其它线程申请锁,就破坏了偏向锁的的假定。

适用场景

偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

  • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
  • 重量级锁:有实际竞争,且锁竞争时间长。

另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。

参考资料

  1. 浅谈偏向锁、轻量级锁、重量级锁
小礼物走一走,来 Github 关注我