线程安全与锁优化

16

线程安全与锁优化

线程安全

定义: 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称为这个对象是线程安全的。

Java语言中的线程安全

按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

不可变:不可变的对象一定是线程安全的,不可变对象外部的可见状态永远不会改变。在Java中,例如String、Number及其子类(Long、Double、BigInteger、BigDecimal)都是不可变的对象。

线程绝对安全:线程绝对安全符合线程安全的定义,但实现起来代价非常高昂。

相对线程安全: 一般意义上的线程安全指的是相对线程安全,它需要保证对这个对象的单次操作是线程安全的。如Vector、Hashtable、Collections提供的synchronizedCollection()等。

线程兼容: 线程兼容指对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段保证对象在并发环境下可以被正确使用。此类对象如HashMap等。

线程对立: 指不管调用端是否采取了同步措施,都无法在多线程环境下并发使用代码。此种情况非常少见。

线程安全的实现方法

互斥同步
在Java中,最常见的实现方法是使用synchronized关键字,还有Lock。

ReentrantLock与synchronized相比增加了一些高级功能,主要有:

  • 等待可中断:指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 公平锁
  • 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象。

当ReentrantLock和synchronized同时满足条件时,优先使用synchronized,原因如下:

  • synchronized是在java语法层面的同步,足够清晰,也足够简单
  • Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中跑出异常,则有可能永远不会释放持有的锁。synchronized可以由java虚拟机来确保即时抛出异常,也能释放锁。
  • 从长远来看,Java虚拟机更容易针对synchronized来进行优化。

非阻塞同步
使用乐观锁来进行非阻塞同步,但是需要操作系统的指令支持,如CAS指令等。

无同步方案

锁优化

JVM锁优化包括适应性自旋、锁消除、锁膨胀、轻量级锁、偏向锁等。

自旋锁与适应性自旋

引入自旋旋的目的是为了避免线程被频繁调度,如果一个线程持有锁的时间很短,那么另一个线程完全可以通过自旋等待一段时间而获得锁,从而避免阻塞线程。这样做减少了线程从用户态陷入内核态导致的上下文切换,而该过程是耗时的。
自旋锁: 让一个线程处于忙循环,这就是所谓的自旋锁,自旋次数默认为10次。
适应性自旋: JDK1.6引入,适应性自旋的自旋次数是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得了锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对较长时间。相反的,如果上次自旋等待未获得锁,那么以后想要获得这个锁,可能不会再进入自旋状态。

锁消除

在逃逸分析理论中,将逃逸情况分为:无逃逸、无方法逃逸、无线程逃逸。如果一个对象经过逃逸分析后,发现它无线程逃逸,那么就可以认为该对象无争用情况发生(属于线程私有的对象),进而可以对该对象上的锁进行消除。

锁粗化

如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这被称为锁粗化。

偏向锁

偏向锁的mark_word字段会保存当前持有该偏向锁的线程ID,会将原来用于存储hash_code的位置占用。那么对一个偏向锁对象调用hashCode方法会发生什么呢?

  1. 当一个对象已经计算过一致性哈希后,它就无法再进入偏向锁状态了
  2. 而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希请求时,它的偏向状态会被立即撤销,锁会膨胀为重量级锁。在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态下的Mark Word,其中自然可以存储原来的哈希码。