JVM

JVM高级特性-线程同步及锁优化

Posted by YaPi on April 21, 2017

线程安全实现

互斥同步

主要实现方式:临界区、互斥量和信号量都是主要的互斥实现方式

在java中,最基本的同步手段就是synchronized关键字,synchronized关键字在经过编译过后,会在同步块前后分别形成monitorenter和monitorexit两个字节码指令。这两个指令都需要一个reference类型的参数来指明要锁定和解锁的对象。指定了对象参数就取对应对象参数,没有指定就根据修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

java的线程是映射到操作系统的原生线程之上的,如果要阻塞或者唤醒一个线程,都需要操作系统帮忙完成,这就需要从用户态转换到核心态中,状态转换花费的时间通常比较长,所以,虚拟机本身会进行一些优化,比如,再通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁的切入到核心态之中

还可以使用ReentrantLock来实现同步,ReentrantLock表现为API层面上得互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面得互斥。同时具有三项高级功能:等待可中断、可实现公平锁、以及锁可以绑定多个条件

等待可中断是指持有锁在长期获取不到锁得时候,可以选择放弃等待,改为处理其他事情。

公平锁是指获取锁得顺序和申请锁得时间顺序绑定,ReentrantLock默认是公平锁

绑定多个条件是指ReentrantLock可以同时绑定多个condition对象。

非阻塞同步

互斥同步是一种悲观的并发策略,无论共享数据是否真的会出现竞争,它都要进行加锁。这样会对性能产生严重得影响。所以就有了乐观得并发策略,非阻塞同步。它是一种基于冲突检测的策略(依靠硬件得发展–原子操作的实现)

最主要得硬件指令支持 CAS (compare and swap),1.5之后支持。

CAS需要3个操作数,分别是内存位置,旧得预期值和新值。在执行指令时,当且仅当内存位置得值是旧得预期值时,才更新为新值

CAS得ABA问题:原位置得值被修改为其他值了,在比较判断得时候又改回了原来的值,但是CAS操作还是会成功。

锁优化

jdk1.5到jdk1.6得一个重要改进就是高效并发。HotSpot虚拟机开发团队优化了各种锁,如:自适应自旋、锁消除、锁粗话、轻量级锁和偏向锁等

自旋和自适应自旋

互斥同步对性能最大得影响是阻塞得实现,挂起和恢复线程的操作都需要转入到内核态完成,这些操作非常耗时。同时,在多数情况下,共享数据得锁定状态只会持续很短的一段时间,为了这段时间取挂起和恢复线程并不值得。在这种情况下,若有两个线程在请求一个锁,其中一个获得了,就可以让另外一个稍等一下,但不放弃处理器执行时间,看看持有锁得线程是否很快就释放锁。这种技术就是自旋锁。

自旋锁并不能完全代替阻塞,有些情况,因为自旋还是会消耗cpu资源的,若锁持有者长时间不释放锁,那么就会造成无谓的消耗。所以默认自旋次数10次,可设置。

自适应自旋,意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。若果某个自旋锁前面获取到了锁,那么他会认为接下来的自旋锁也有机会获得锁,会允许其自旋更长的时间。若很少成功过,那么后续就会忽略调自旋过程。

轻量级锁与偏向锁

在没有多线程竞争的前提下,减少系统互斥量操作产生的性能消耗

实现主要依赖于对象头里的数据。我们知道,对象头(Mark Word)内部主要有两部分数据,一部分存储GC分代年龄,哈希吗等,另一部分存储指向方法区该对象类型数据的指针,如果是数组对象的话还会有一个额外的部分用于存储数组长度。

当代码进入同步块时,若该对象的对象头对应标志对象锁定状态为未锁定状态时,虚拟机将在当前线程中创建一个Mark Word的拷贝。然后,采用CAS操作尝试讲对象的Mark Word 更新为指向该拷贝的指针,若成功,则该对象拥有了对象的锁。并且对象的Mark Word的锁标志位会变为00,及表示当前对象处于轻量级锁定状态。若更新失败,判断该对象的Mark Word是否指向当前线程的栈帧,若是,则证明已经获取了锁,则可继续执行,否则,说明这个对象已被别人占领,同时有两条以上的线程竞争锁时,轻量级锁不再有效,膨胀为重量级锁,锁标志为变为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

解锁的过程是一样的,使用CAS操作去替换对象的Mark Word为线程栈帧的记录,若失败了,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

偏向锁

经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引人了偏向锁

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

  1. 当锁对象第一次被线程获取的时候,虚拟机会将对象头中的锁标志位置为 “01”(偏向模式)
  2. 同时,使用CAS操作,把获取到这个锁的线程的ID记录在对象的Mark Word中
  3. 如果 CAS成功,持有偏向锁的线程每次进入这个锁相关的同步块时,虚拟机可以不进行任何同步操作

若此时有其他线程争夺锁

  1. 首先暂停拥有偏向锁的线程
  2. 然后检查持有偏向锁的线程是否活着
    1. 不活跃,将对象头设置成无锁状态 (标志位”01”,但不可偏向)
    2. 活跃
      1. CAS成功,重新偏向,更改线程ID
      2. 失败,恢复成无锁状态,或者变成轻量级锁定状态。

逃逸分析

当一个对象再方法中被定义后,它可能北外部方法所引用,例如作为调用参数传递到其他方法中,成为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以再其他线程中访问的实例变量,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些优化。比如:

  1. 栈上分配,在栈上分配这个对象,那么大量对象会随着入栈和出栈被销毁,不用等到垃圾回收
  2. 同步消除:如果能确认一个变量不会逃逸出线程,那么这个变量就不会有竞争,对这个变量的同步措施就可以清除掉
  3. 标量替换:java中的原始数据类型都不能再进一步分解,这种可以称为标量。其他的被称为聚合量。比如对象,就是一个标准的聚合量。把一个对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。若逃逸分析一个对象不会北外部访问,并且这个对象可以被拆散,那程序真正执行的时候可能不创建这个对象,而改为直接创建它的若干个被方法使用到的成员变量来代替。那这一部分变量,就可以使用栈上分配了