我只是喜欢新玩具,而Java 8有很多 。 这次我想谈谈我的最爱之一-并发加法器。 这是一组用于管理由多个线程编写和读取的计数器的新类。 新的API有望显着提高性能,同时仍然使事情变得简单明了。
自从多核架构问世以来人们一直在管理并发计数器,让我们看一看到目前为止Java提供了哪些选项,以及与新API相比它们的性能如何。
脏计数器 –这种方法意味着您正在多个线程之间的常规对象或静态字段中进行写入/读取操作。 不幸的是,这有两个原因。 首先是在Java中,A + = B操作不是原子操作。 如果打开输出字节码,将至少看到四条指令-一个用于将堆中的字段值加载到线程堆栈中,第二个用于加载增量,第三个用于添加增量,第四个用于设置结果进入领域。
如果在同一个内存位置同时有多个线程同时执行此操作,则极有可能错过写操作,因为一个线程可以覆盖另一个线程的值(又称“读取-修改-写入”) 。 与此相关的还有另一个讨厌的角度,那就是价值的波动性。 下面的更多内容。
这是一个菜鸟错误,而且很难调试。 如果您遇到了在您的应用程序中执行此操作的任何人,我想问一个小小的忙。 在数据库中搜索“ Tal Weiss”。 如果您在那里看到我–删除我的记录。 我会更安全的。
同步 -这是最基本的并发习惯用法,它在读取或写入值时会阻塞所有其他线程。 当它起作用时,这是将代码转换为DMV行的可靠方法。
RWLock –基本Java锁的这种稍微复杂的版本,使您可以区分更改值和需要阻止其他线程的线程与仅读取而不需要关键部分的线程。 尽管这可能更有效(假设编写器的数量很少),但这是一个相当不错的方法,因为在获取写锁时,您将阻止所有其他线程的执行。
易失性 -这个相当容易被误解的关键字实际上指示JIT编译器取消优化运行时机器代码,以便其他线程可以立即看到对该字段的任何修改。
这使某些JIT编译器最喜欢的优化工作失去了按分配分配给内存的顺序进行。 再说一次 你听到了 JIT编译器可能会更改对字段进行分配的顺序。 这种神秘的小策略(也称为before-before )使它可以最小化程序访问全局堆所需的次数,同时仍确保您的代码不受其影响。 偷偷摸摸的…
那么什么时候应该使用易失性计数器? 如果只有一个线程在更新值,而有多个线程在使用它,那么这是一个非常好的策略–根本没有争用。
那么为什么不总是问它呢? 因为当一个以上的线程正在更新该字段时,这不能很好地工作。 由于A + = B不是原子的,因此您有重写别人的写的风险。 在Java 8之前,您需要使用AtomicInteger。
AtomicInteger-这组类使用CAS(比较和交换)处理器指令来更新计数器的值。 听起来不错,不是吗? 好,是的,不是。 这很有效,因为它利用直接的机器代码指令来设置该值,而对其他线程的执行影响最小。 缺点是,如果由于与另一个线程的争用而无法设置该值,则必须重试。 在竞争激烈的情况下,这可能会变成自旋锁,其中线程必须不断尝试并在无限循环中设置该值,直到成功为止。 这不是我们想要的。 输入带有LongAdders的Java 8。
Java 8 Adders –这是一个非常酷的新API,我迫不及待想要了解它! 从使用角度来看,它与AtomicInteger非常相似。 只需创建一个LongAdder并使用intValue()和add()即可获取/设置值。 魔术发生在幕后。
此类的作用是当直接CAS由于争用而失败时,它将增量存储在为该线程分配的内部单元对象中。 然后在调用intValue()时将待处理单元格的值加到总和中。 这减少了返回和CAS或阻止其他线程的需要。 很聪明的东西!
这么好说吧–让我们看看这只小狗在行动。 我们已经建立了以下基准测试:将计数器重置为零,并开始使用多个线程进行读取和递增。 当计数器达到10 ^ 8时停止。 我们在4核i7处理器上运行基准测试。
我们使用总共十个线程来运行基准测试-五个用于写作,五个用于阅读,因此我们在这里势必会引起严重的争论:
- 请注意,肮脏和易变的风险值都将覆盖。
- 代码在这里可用
底线
- 并发加法器洁净室的性能比原子整数提高60-100% 。
- 除了锁定时,添加线程没有什么区别。
- 请注意,使用同步锁或RW锁会给您带来巨大的性能损失-慢一个数量级!
如果您已经有机会在代码中使用这些类,那么我很乐意听到。
- 补充阅读– Brian Goetz关于Java并发性。
翻译自: https://www.javacodegeeks.com/2014/04/java-8-longadders-the-right-way-to-manage-concurrent-counters.html