只要线程之间共享可变变量,就必须使用同步来确保一个线程进行的更新对其他线程及时可见。 同步的主要方法是使用synchronized块,该块提供互斥和可见性保证。 (其他形式的同步包括volatile变量, java.util.concurrent.locks的Lock对象和原子变量。)当两个线程要访问共享的可变变量时,不仅两个线程都必须使用同步,而且如果两个线程都在使用, synchronized块,这些synchronized块必须使用相同的锁定对象。
在实践中,锁定分为两类:大多数是竞争性的,而大多数是非竞争性的。 大多数争用的锁是应用程序中的“热”锁,例如用于保护线程池的共享工作队列的锁。 许多线程不断需要这些锁保护的数据,并且可以预期,当您要获取此锁时,可能必须等到其他人完成后再使用它。 大多数情况下,无竞争锁是保护那些不会被如此频繁访问的数据的锁,因此,大多数情况下,当线程去获取该锁时,没有其他线程持有该锁。 大多数锁不经常争用,因此提高无竞争锁的性能可以显着提高整体应用程序性能。
JVM具有分别用于竞争(“慢路径”)和非竞争(“快速路径”)锁获取的代码路径。 优化快速路径已经付出了很多努力。 野马进一步改善了快速路径和慢速路径,并增加了许多优化措施,可以完全消除一些锁定。
Java内存模型说,一个线程退出同步块发生-在另一个线程进入受同一锁保护的同步块之前 ; 这意味着线程A退出受锁M保护的synchronized块时,对线程A可见的任何内存操作,当线程B进入受M保护的synchronized块时,对于线程B可见,如图1所示。对于使用不同锁的synchronized块,我们不能对它们的顺序做任何假设-好像根本没有同步。
因此,有理由推论,如果某个线程进入一个受锁保护的synchronized块,而其他线程将无法对其进行同步,则该同步将不起作用,因此可以被优化器删除。 ( Java语言规范明确允许这种优化。)这种情况听起来不太可能,但是在某些情况下编译器很清楚。 清单1显示了线程本地锁定对象的简化示例:
因为对锁对象的引用在任何其他线程可能使用它之前就消失了,所以编译器可以告知可以删除上述同步,因为两个线程不可能使用同一锁进行同步。 尽管没有人会直接使用清单1中的习惯用法,但是此代码与可以证明与synchronized块关联的锁是线程局部变量的情况非常相似。 “ Thread-local”并不一定意味着它是通过ThreadLocal类实现的。 它可以是编译器可以证明从未被另一个线程访问的任何变量。 由局部变量引用的且从未脱离其定义范围的对象满足此测试-如果某个对象被限制在某个线程的堆栈中,则其他任何线程都无法看到对该对象的引用。 (对象的跨线程共享的唯一方法是将对对象的引用发布到堆中。)
幸运的是,我们上个月讨论的转义分析为编译器提供了所需的信息,以优化使用线程本地锁对象的synchronized块。 如果编译器可以证明(使用转义分析)某个对象从未发布到堆中,则该对象必须是线程局部对象,因此,使用该对象作为锁的任何synchronized块在Java内存模型下均无效( JMM),可以删除。 这种优化称为锁消除 ,它是针对Mustang的另一种JVM优化。
与线程本地对象锁定的使用比您想象的要频繁得多。 有很多类,例如StringBuffer和java.util.Random ,它们是线程安全的,因为它们可以在多个线程中使用,但是它们通常以线程局部方式使用。
考虑清单2中的代码,该代码在建立字符串值的过程中使用Vector 。 getStoogeNames()方法创建一个Vector ,向其添加多个字符串,然后调用toString()将其转换为字符串。 每个对Vector方法之一的调用(对add()三个调用和对toString()三个调用toString()需要获取并释放对Vector的锁定。 尽管所有锁获取都是无竞争的,因此速度很快,但编译器实际上可以使用锁省略功能完全消除同步。
因为没有对Vector引用曾经逃过getStoogeNames()方法,所以它必然是线程局部的,因此,将其用作锁的任何synchronized块在JMM下均无效。 编译器可以内联对add()和toString()方法的调用,然后它将认识到它正在获取并释放对线程本地对象的锁定,并且可以优化所有四个锁定-解锁操作。
过去我曾说过,尝试避免同步通常是一个坏主意。 诸如锁删除之类的优化的到来又增加了一个避免尝试消除同步的理由-编译器可以在安全的地方自动执行此操作,否则将其保留在原处。
除了转义分析和锁定消除功能之外,Mustang还对锁定性能进行了其他一些优化。 当两个线程争用一个锁时,其中一个将获得锁,而另一个将不得不阻塞直到锁可用。 有两种明显的实现阻塞的技术: 让操作系统挂起该线程,直到以后将其唤醒或使用自旋锁为止。 自旋锁基本上等于以下代码:
while (lockStillInUse) ;虽然旋转锁是CPU密集型和低效率的出现,他们可以比挂起线程,并随后将其唤醒,如果有问题的锁被维持了很短的时间更有效率。 挂起和重新安排线程会产生大量开销,这涉及JVM,操作系统和硬件的工作。 这给JVM实现者带来了一个问题:如果锁只保留很短的时间,那么旋转会更有效; 如果长时间保持锁,则挂起效率更高。 由于没有关于给定应用程序锁定时间的分布情况的信息,大多数JVM都是保守的,并且在无法获取锁定时只是挂起线程。
但是,可以做得更好。 对于每个锁,JVM可以根据过去获取的行为在旋转和暂停之间进行自适应选择。 它可以尝试旋转,如果在短时间内成功,则继续使用该方法。 另一方面,如果一定数量的旋转未能获取该锁,则可以确定这是通常“长时间”持有的锁,然后重新编译该方法以仅使用悬挂。 该决定可以基于每个锁或每个锁站点进行。
这种自适应方法的结果是更好的整体性能。 在JVM花了一些时间来获取有关锁使用模式的一些性能分析信息之后,它可以对通常保留很短时间的锁使用旋转,对通常保留很长时间的锁使用暂停。 在静态编译的环境中不可能进行这样的优化,因为有关锁使用模式的信息在静态编译时不可用。
可以用来降低锁定成本的另一种优化方法是锁定粗化 。 锁定粗化是合并使用同一锁定对象的相邻同步块的过程。 如果编译器无法使用锁省略消除锁,则可以通过使用锁粗化来减少开销。
清单3中的代码不一定是锁定addStoogeNames()的候选者(尽管addStoogeNames()联addStoogeNames() ,JVM仍可能能够消除锁定),但仍可以从锁定粗化中受益。 对add()的三个调用在获取Vector的锁,执行操作和释放锁之间交替。 编译器可以观察到有一系列相邻的块在同一锁上操作,并将它们合并为一个块。
这不仅减少了锁定开销,而且通过合并这些块,将三个方法调用的内容转换为一个大的synchronized块,从而使优化器可以使用更大的基本块。 因此,锁定粗化还可以启用与锁定无关的其他优化。
即使在synchronized块或方法调用之间还有其他语句,编译器仍可以执行锁粗化。 允许编译器将语句移到synchronized块中,但不能移出。 因此,如果在对add()的调用之间存在中间语句,则编译器仍可以将整个addStooges()转换为一个将Vector作为锁定对象的大synchronized块。
锁定粗化可能需要在性能和响应能力之间进行权衡。 通过将三个锁定/解锁对组合为单个锁定/解锁对,可以通过减少指令数量和减少内存总线上的同步通信量来提高性能。 这样做的代价是,可能会延长持有该锁的持续时间,从而增加其他线程可能被锁定的时间,并且还会增加锁争用的可能性。 但是,在任何一种情况下,都将锁定保持相对较短的时间,并且编译器可以基于受同步保护的代码的长度来应用启发式方法,以在此处进行合理的权衡。 (并且取决于较大的基本块还启用了哪些其他优化,可能甚至在粗化的情况下也不会延长锁定的持续时间。)
几乎每个JVM版本都提高了无与伦比的锁定性能。 野马通过改善无竞争锁和无竞争锁的原始性能并引入可以消除许多锁操作的优化来延续这一趋势。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp10185/index.html
相关资源:野马之歌_精美学习课件ppt