虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > 虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)

虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)

 2011/10/11 8:12:20  zhangyiqian  http://zhangyiqian.iteye.com  我要评论(0)
  • 摘要:转载自----http://www.iteye.com/topic/1018932高效并发是JDK1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(AdaptiveSpinning)、锁削除(LockElimination)、锁膨胀(LockCoarsening)、轻量级锁(LightweightLocking)、偏向锁(BiasedLocking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题
  • 标签:简介 优化 虚拟机

转载自 ---- http://www.iteye.com/topic/1018932

?

  高效并发是JDK 1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁削除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

13.3.1 自旋锁与自适应自旋

  前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系 统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程 并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时 间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
  自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 1.6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的, 所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作, 反而会带来性能的浪费。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次 数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。
  在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象 上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间, 比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自 旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。

13.3.2 锁削除

  锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于 逃逸分析的数据支持(第11章已经讲解过逃逸分析技术),如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈 上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
  也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的 情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。我们来看看下面代码 清单13-6中的例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。

  代码清单 13-6 一段看起来没有同步的代码

Java代码 ?收藏代码
  1. public ?String?concatString(String?s1,?String?s2,?String?s3)?{??
  2. ????return ?s1?+?s2?+?s3;??
  3. }??

   我们也知道,由于String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String 连接做自动优化。在JDK 1.5之前,会转化为StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuilder对象的连续append()操作。即代码清单13-6中的代码可能会变成代码清单13-7的样 子 。

  代码清单 13-7 Javac转化后的字符串连接操作

Java代码 ?收藏代码
  1. public ?String?concatString(String?s1,?String?s2,?String?s3)?{??
  2. ????StringBuffer?sb?=?new ?StringBuffer();??
  3. ????sb.append(s1);??
  4. ????sb.append(s2);??
  5. ????sb.append(s3);??
  6. ????return ?sb.toString();??
  7. }??

(注1:实事求是地说,既然谈到锁削除与逃逸分析,那虚拟机就不可能是JDK 1.5之前的版本,所以实际上会转化为非线程安全的StringBuilder来完成字符串拼接,并不会加锁。但是这也不影响笔者用这个例子证明Java对象中同步的普遍性。)

  现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象。虚拟机观察 变量sb,很快就会发现它的动态作用域被限制在concatString()方法内部。也就是sb的所有引用永远不会“逃逸”到 concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地削除掉,在即时编译之后,这段代码就会忽略掉所有的同 步而直接执行了。

13.3.3 锁膨胀

  原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快地拿到锁。
  大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
  上面代码清单13-7中连续的append()方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同 步的范围扩展(膨胀)到整个操作序列的外部,以代码清单13-7为例,就是扩展到第一个append()操作之前直至最后一个append()操作之后, 这样只需要加锁一次就可以了。

13.3.4 轻量级锁

  轻量级锁是JDK 1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。首先需要强 调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从HotSpot虚拟机的对象(对象头部分)的内存布局开始介绍。 HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32个和64个Bits,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组 长度。
  对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如表13-1所示。

  表13-1 HotSpot虚拟机对象头Mark Word

存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC标记 偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向


  简单地介绍完了对象的内存布局,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),这时候线程堆栈与对象头的状态如图13-3所示。


  图13-3 轻量级锁CAS操作之前堆栈与对象的状态



  然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个Bits)将转变为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图13-4所示。


  图13-4 轻量级锁CAS操作之后堆栈与对象的状态


注2:图13-3和图13-4来源于HotSpot虚拟机的一位Senior Staff Engineer——Paul Hohensee所写的PPT《The Hotspot Java Virtual Machine》

  如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占 了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
  上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
  轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使 用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级 锁更慢。

13.3.5 偏向锁

  偏向锁也是JDK 1.6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去 消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
  偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
  如果读者读懂了前面轻量级锁中关于对象头Mark Word与线程之间的操作过程,那偏向锁的原理理解起来就会很简单。假设当前虚拟机启用了偏向锁(启用参数-XX:+UseBiasedLocking, 这是JDK 1.6的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的 线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、 Unlocking及对Mark Word的Update等)。
  当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级 锁的状态转化及对象Mark Word的关系如图13-5所示。


  图13-5 偏向锁、轻量级锁的状态转化及对象Mark Word的关系



  偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并不一定总是对程序运行有利,如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分 析的前提下,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。

?

?

?

大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。
原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。
互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。

为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。

轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。
它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。

本文将详细介绍JVM如何利用CAS,实现轻量级锁。

?

Java轻量级锁原理详解(Lightweight Locking)

?

大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。
原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。
互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。

为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。

轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。
它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。

本文将详细介绍JVM如何利用CAS,实现轻量级锁。

?

原理详解

Java Object Model中定义,Object Header是一个2字(1 word = 4 byte)长度的存储区域。
第一个字长度的区域用来标记同步,GC以及hash code等,官方称之为?mark word 。第二个字长度的区域是指向到对象的Class。

在2个word中,mark word是轻量级锁实现的关键。它的结构见下表

从表中可以看到,state为lightweight locked的那行即为轻量级锁标记。bitfieds名为指向lock record的指针,这里的lock record,其实是一块分配在线程堆栈上的空间区域
用于CAS前,拷贝object上的mark word(为什么要拷贝,请看下文)。
第三项是重量级锁标记。后面的状态单词很有趣,inflated,译为膨胀,在这里意思其实是锁已升级到OS-level。
在本文的范围内,我们只关注第二和第三项即可。

为了能直观的理解lock,unlock与mark word之间的联系,我画了一张流程图:

在图中,提到了拷贝object mark word,由于脱离了原始mark word,官方将它冠以displaced前缀,即displaced mark word (置换标记字)。
这个displaced mark word是整个轻量级锁实现的关键,在CAS中的compare就需要用它作为条件。

为什么要拷贝mark word?
其实很简单,原因是为了不想在lock与unlock这种底层操作上再加同步。

在拷贝完object mark word之后,JVM做了一步交换指针的操作 ,即流程中第一个橙色矩形框内容所述。
将object mark word里的轻量级锁指针指向lock record所在的stack指针,作用是让其他线程知道,该object monitor已被占用。
lock record里的owner指针指向object mark word的作用是为了在接下里的运行过程中,识别哪个对象被锁住了。

下图直观地描述了交换指针的操作。

exchange_pointer_1

最后一步unlock中,我们发现,JVM同样使用了CAS来验证object mark word在持有锁到释放锁之间,有无被其他线程访问。
如果其他线程在持有锁这段时间里,尝试获取过锁,则可能自身被挂起,而mark word的重量级锁指针也会被相应修改。
此时,unlock后就需要唤醒被挂起的线程。

?

Java偏向锁实现原理(Biased Locking)

阅读本文的读者,需要对Java轻量级锁有一定的了解,知道lock record, mark word之类的名词。可以参考我的一篇博文:Java轻量级锁原理详解(Lightweight Locking)

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

轻量级锁也是一种多线程优化,它与偏向锁的区别在于,轻量级锁是通过CAS来避免进入开销较大的互斥操作,而偏向锁是在无竞争场景下完全消除同步,连CAS也不执行(CAS本身仍旧是一种操作系统同步原语,始终要在JVM与OS之间来回,有一定的开销)。

所谓的无竞争场景,举个例子,就是单线程访问带同步的资源或方法。

?

偏向锁实现原理

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)

通过下图可以更直观的理解偏向锁:

这张图,省略了轻量级锁相关的几处步骤,将关注点更多地聚焦在偏向锁的状态变化上。
偏向模式和非偏向模式 ,在下面的mark word表中,主要体现在thread ID字段是否为空。

挂起持有偏向锁的线程 ,这步操作类似GC的pause,但不同之处是,它只挂起持有偏向锁的线程(非当前线程)。

在抢占模式的橙色区域说明中有提到,指向当前堆栈中最近的一个lock record(在轻量级锁原理一文有讲到,lock record是进入锁前会在stack上创建的一份内存空间)。
这里提到的最近的一个lock record,其实就是当前锁所在的stack frame上分配的lock record。
整个步骤是从偏向锁恢复到轻量级锁的过程。

偏向锁也会带来额外开销

在JDK6中,偏向锁是默认启用的。它提高了单线程访问同步资源的性能。
但试想一下,如果你的同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。事实上,消除偏向锁的开销还是蛮大的。

所以在你非常熟悉自己的代码前提下,大可禁用偏向锁 -XX:-UseBiasedLocking 。

?

?

?

发表评论
用户名: 匿名