《深入理解Java虚拟机》读书笔记 - 线程安全与锁优化

此篇为《深入理解Java虚拟机》第十三章13.2部分的读书笔记

线程安全

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

Java 语言中的线程安全

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

  • 不可变

在 Java 中不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采用任何的线程安全保障措施。如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它的不可变性;如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响,如 String。

  • 绝对线程安全

在 Java 中要求一个类如同开头的定义一般,不管运行环境如何,调用者都不需要任何额外的同步措施。这种做法虽然是安全可用的,但是这往往都会付出很大的、甚至是不切实际的代价。

  • 相对线程安全

相对的线程安全就是我们通常意义上的所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要额外的保证措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段保证调用的正确性。例如 Vector、Hashtable、Collections.synchronizedCollection() 方法包装的集合等。

特别说明,Vector 内部函数都使用 synchronized 关键字修饰,看上去很安全,但如果调用者的操作不当,仍会出现不可避免的错误。即在查询一个元素的时候,某个线程就已经将这个元素删除了,那就会抛出 ArrayIndexOutOfBoundsException 异常。

  • 线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下中可以安全使用。例如 Java API 中大部分的类都属于线程兼容,如与前面 Vector、Hashtable 内部所使用的就是 ArrayList 和 HashMap 等。

  • 线程对立

线程对立是指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常是有害的,应当尽量避免。比如 Thread 类中的 suspend() 和 resume() 方法,suspend() 试图中断线程,resume() 试图恢复线程,如果并发进行的话,会存在很大的死锁风险,所以这两个方法已被抛弃(@Depreacted)使用。

线程安全的实现方法

P390

锁优化

自 JDK1.5之后,HotSpot 虚拟机针对多线程并发花了十分的精力,去实现各种锁优化技术,如适应性自选(Adaptive Spinning)、锁清除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。

自旋锁与自适应自旋

在线程互斥同步的时候,由于需要实现线程互斥,被阻塞线程需要由运行态转入阻塞态,而挂起线程和恢复线程的操作都需要从用户态转入到内核态中完成,这些操作给系统的并发性能带来了很大的压力。而往往线程并发时,线程共享数据的锁定状态只会持续很短的一段时间,为了这段时间选择去挂起和恢复线程是不值得的。

那么就引出了自旋锁的作用:如果在同一时刻中有两个以上的线程并行执行,我们可以让后面请求锁的线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快地就会释放锁。但持有锁的线程依旧不放弃锁,那么为了最大化降低 CPU 的消耗,将正在自旋等待的线程使用传统的方式进行挂起阻塞等待。上面所述中,为了让线程等待,我们只需要让线程执行一个忙循环(自旋)即可。

而在 JDK1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。

锁粗化

锁粗化是指虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,那将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。例如,在 for 循环中进行对字符串拼接的任务进行加锁,那么锁粗化就会将这一操作外提至 for 循环外。

轻量级锁

轻量级锁是相对于传统的锁机制操作而言的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁同步代码所带来的性能消耗。轻量级锁本质上是一种乐观锁的实现。

偏向锁

偏向锁是指在无竞争情况下,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。如果轻量级锁是在无竞争情况下使用 CAS 操作去消除同步的互斥量,那么偏向锁就是在无竞争的情况下,把整个同步都消除掉,连 CAS 操作都不需要。