加入收藏 | 设为首页 | 会员中心 | 我要投稿 威海站长网 (https://www.0631zz.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 资源网站 > 空间 > 正文

深入理解Java虚拟机(高效并发)

发布时间:2019-07-25 09:07:09 所属栏目:空间 来源:张磊BARON
导读:高效并发是 JVM 系列的最后一篇,本篇主要介绍虚拟机如何实现多线程、多线程间如何共享和竞争数据以及共享和竞争数据带来的问题及解决方案。 一. Java 内存模型与线程 让计算机同时执行多个任务,不只是因为处理器的性能更加强大了,更重要是因为计算机的

这个原子性靠什么来保证呢?如果这里再使用互斥同步来保证原子性就失去意义了,所以我们只能靠硬件来完成这件事,保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,简称 CAS)
  • 加载链接/条件存储(Load-Linked/Store-Conditional,简称 LL/SC)

前三条是之前的处理器指令集里就有的,后两条是新增的。

CAS 指令需要 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示)。CAS 执行指令时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则他就不执行更新,但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。

在 JDK 1.5 之后,Java 程序中才可以使用 CAS 操作,该操作由 sun.misc.Unsafe 类里的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器 CAS 指令,没有方法的调用过程,或者可以认为是无条件内联进去了。

由于 Unsafe 类不是提供给用户程序调用的类,因此如果不用反射,我们只能通过其他的 Java API 来间接使用,比如 J.U.C 包里的整数原子类,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。

尽管 CAS 看起来很美,但是这种操作却无法覆盖互斥同步的所有场景,并且 CAS 从语义上来说并不是完美的。如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查它仍然是 A 值,那我们就能说它的值没有被其他线程修改过吗?如果在这段时间内曾经被改为了 B,后来又被改回为 A,那 CAS 操作就会认为它从来没有被改变过。这个漏洞称为 CAS 操作的「ABA」问题。

为了解决「ABA」问题,J.U.C 包提供了一个带有标记的原子引用类 AtomicStamoedReference,它可以通过控制变量值的版本来保证 CAS 的正确性。不过这个类比较「鸡肋」,大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

无同步方案

要保证线程安全不一定要进行同步,如果一个方法本来就不涉及共享数据,那它自然无需任何同步措施,因此会有一些代码天生就是线程安全的,其中就包括下面要说的可重入代码和线程本地存储。

可重入代码(Reentrant Code):也叫纯代码,可以在代码执行的任何时候中断它,转而去执行另一端代码(包括递归调用自己),而在重新获得控制权后,原来的程序不会出现任何错误。可重入代码有一些共同特征,例如不依赖存储在堆上的数据和公用的系统资源,用到的状态量都由参数传入、不调用非可重入的方法等。如果一个方法的返回结果可以预测,只要输入相同,就能返回相同的输出,那它就是可重入代码,当然也就是线程安全的。

线程本地存储(Thread Local Storage):也就是说这个数据是线程独有的,ThreadLocal 就是用来实现线程本地存储的。

2.锁优化

HotSpot 虚拟机开发团队花费了很大的精力实现了各种锁优化,比如自旋锁与自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等。

自旋锁与自适应自旋

自旋锁前面我们在聊互斥同步的时候就提到过,互斥同步对性能最大的影响就是阻塞的实现,挂起线程和恢复线程都涉及到了用户态到内核态的转换,这种状态的转换会给系统并发性能带来很大的压力。但是大多数场景下,共享数据的锁定状态只会持续很短的一段时间,为了这短暂的时间去挂起和恢复线程显得不那么划算。如果物理机有一个以上的处理器,能让两个或以上的线程同时并行处理,我们就可以让后面请求锁的那个线程「稍等一下」,但是不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要执行一个空转的循环(自旋),这就是所谓的自旋锁。

自旋等待虽然避免了线程切换的开销,但是它要占用处理器的时间。如果锁被占用的时间很短,那么自旋等待的效果当然很好;反之,如果锁被占用的时间很长,那么自旋的线程就会白白消耗处理器资源,反而形成负优化。所以自旋等待必须有个限度,但是这个限度如果设置一个固定值并不是最有选择,因此虚拟机开发团队设计了自适应自旋锁,让自旋等待的时间不再固定,而是由前一次在同一个锁上自旋的时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也有可能会成功,会将自旋等待的时间延长。如果对于某个锁,自旋等待很少成功获得过,那在以后要获取这个锁的时候就会放弃自旋。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确。

锁消除

即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁就会进行锁消除。所消除的主要判定依据来源于逃逸分析的数据支持,如果判定一段代码中,堆上的所有数据都不会逃逸出去从而被其它线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就没必要了。

锁粗化

我们在编码时,总是推荐将同步块的作用范围限制到最小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要的同步操作数量尽可能变小,如果存在竞争,那等待锁的线程也能尽快拿到锁。通常,这样做是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁的进行互斥同步也会导致不必要的性能损耗。那加锁出现在循环体中来举例,虚拟机遇到这种情况,就会把加锁同步的范围扩展(粗化)到循环体外,这样只要加锁一次就可以了,这就是锁粗化。

关于轻量级锁和偏向锁这里就不再介绍,如果大家有兴趣可以留言反馈,我在单独发文介绍。

(编辑:威海站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

推荐文章
    热点阅读