【深入理解JAVA虚拟机】第5部分.高效并发.2.线程安全和锁优化

1 概述

对于这部分的主题“高效并发”来讲,首先需要保证并发的正确性,然后在此基础上实现高效。

2 线程安全

《Java Concurrency In Practice》 的作者Brian Goetz对“线程安全”有一个比较恰当的定义:

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

这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。

2.1 Java语言中的线程安全

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

1.不可变

基本数据类型:定义时使用final关键字修饰它就可以保证它是不可变的。

对象:把对象中带有状态的变量都声明为final。

符合不可变要求的类型:String,枚举,java.lang.Number的部分子类,Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型。但同为Number的子类型的原子类AtomicInteger和AtomicLong则并非不可变的。

2.绝对线程安全
3.相对线程安全

绝对的线程安全完全满足Brian Goetz给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的,甚至有时候是不切实际的代价。

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

我们常说的线程安全类型就是相对线程安全的,如Vector、 HashTable、Collections的synchronizedCollection()方法包装的集合等。

尽管这里使用到的Vector的get()、 remove()和size()方法都是同步的,但对vector遍历仍然是不安全的。

4.线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。

如:ArrayList和HashMap

5.线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。 由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

2.2 线程安全的实现方法

1.互斥同步(Mutual Exclusion&Synchronization)/阻塞同步(Blocking Synchronization)

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。

而互斥是实现同步的一种手段,临界区(CriticalSection)、 互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题.

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。

实现互斥的两种方法:

synchronized

前文讲过,synchronized会解析成monitorenter和monitorexit这两个字节码指令,进而生成lock和unlock内存访问指令,达到互斥目的。

ReentrantLock

相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、 可实现公平锁,以及锁可以绑定多个条件。

两者选择:在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

互斥的缺点

这是一个悲观的同步方式,不论是否发生冲突,都会触发锁操作,而其他线程,则会被阻塞。由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。 所以synchronized是Java语言中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。 而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。

2.非阻塞同步(Non-Blocking Synchronization)

为了解决互斥的缺点,引入了非阻塞同步。

实现的原理:基于冲突检测的乐观并发策略。通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)。

以下操作命令需要硬件来保证操作和冲突检测这两个步骤具备原子性:
  测试并设置(Test-and-Set)。
  获取并增加(Fetch-and-Increment)。
  交换(Swap)。
  比较并交换(Compare-and-Swap,下文称CAS)。
  加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)。

这些命令在sun.misc.Unsafe中,而这个类需要Bootstrap ClassLoader加载,所以用户无法执行,只能使用jdk封装好的类:AtomicXXX

incrementAndGet()方法的JDK源码:

for(;)

{

  int current=get();

  int next=current+1;

  if(compareAndSet(current,next))  //不停重试,直到成功。

    return next;

}

CAS操作的漏洞

“ABA”问题,即别的线程把A改成B,又改成A,但检测的时候,不知道别人已经改过了。

J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。

3.无同步方案

可重入代码(Reentrant Code)

也叫做纯代码(Pure Code),就是函数式编程的那种函数,不依赖外部数据,只受入参影响的代码段。

线程本地存储(Thread Local Storage)

共享数据的可见范围限制在同一个线程之内,无须同步也能保证线程之间不出现数据争用的问题。

其中最重要的一个应用实例就是经典Web交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式 : java.lang.ThreadLocal类.

3 锁优化

3.1 自旋锁与自适应自旋

问题:互斥导致的阻塞,并进一步导致的线程调度,线程从内核态到用户态的转换是很影响性能的,而我们的大多数锁又是只锁一小段代码,很快就执行完了,为了这段时间去挂起和恢复线程并不值得。

解决:遇到互斥锁不急着挂起线程,而是"稍等一下",看锁是否很快被释放了。为了让线程等 待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

  自旋次数的默认值是10次,用户可 以使用参数-XX:PreBlockSpin来更改。 达到次数后,再挂起。

优化:运行中根据数据统计分析,自适应修改自旋次数,称之为"自适应自旋"

3.2 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能 存在共享数据竞争的锁进行消除。

为啥会有实际不需要的同步呢?很多不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了 大部分读者的想象。

比如:StringBuffer是同步的,实际中,大部分声明的都是个局部对象。

3.3 锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反 复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥 同步操作也会导致不必要的性能损耗。

3.4 轻量级锁

传统的互斥锁,都成为"重量级锁"

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

实现思想,还是CAS乐观处理,假设大多数场景都不会发生互斥,所以只在对象头记个标志。当发生抢占时,再升级为普通锁。详见书。

3.5 偏向锁

说轻量级锁是在无竞争的情况下使用CAS操作去消 除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都 不做了。

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

当锁对象第一次被线程获取的时候, 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。

同时使用CAS操作把获取到这个锁 的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次 进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、 Unlocking 及对Mark Word的Update等)

偏向锁可以提高带有同步但无竞争的程序性能。

上一篇:trailingZeroes


下一篇:深入理解Java虚拟机(九)——后端编译与优化