基于锁与无锁并发算法


JSR 166 https://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
https://jcp.org/en/jsr/detail?id=166
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html

​ 原文链接 https://mechanical-sympathy.blogspot.com/2013/08/lock-based-vs-lock-free-concurrent.html

测试用例

为了比较实现,我需要一个不支持特定方法的 API 测试用例。例如,API 应该是无垃圾的并允许方法是原子的。一个简单的测试用例是设计一个可以在二维空间周围移动的宇宙飞船,其位置坐标可以以原子方式读取。每个事务至少需要读取或写入 2 个字段才能使并发有趣。

/**
 * 与可以移动的船的并发表示的接口
 * 一个二维空间,同时执行更新和读取。
 */
public interface Spaceship
{
    /**
     * Read the position of the spaceship into the array of coordinates provided.
     *
     * @param coordinates into which the x and y coordinates should be read.
     * @return the number of attempts made to read the current state.
     */
    int readPosition(final int[] coordinates);

    /**
     * Move the position of the spaceship by a delta to the x and y coordinates.
     *
     * @param xDelta delta by which the spaceship should be moved in the x-axis.
     * @param yDelta delta by which the spaceship should be moved in the y-axis.
     * @return the number of attempts made to write the new coordinates.
     */
    int move(final int xDelta, final int yDelta);
}

通过分解一个不可变的 Position 对象,上面的 API 会更干净,但我想保持它无垃圾,并创建以最小间接更新多个内部字段的需要。这个 API 可以很容易地扩展到 3 维空间,并且要求实现是原子的。

为每艘宇宙飞船构建了多个实现,并由测试工具执行。此博客的所有代码和结果都可以在这里找到。测试工具

将依次运行每个实现,方法是使用超态调度模式来尝试并防止在访问并发方法时内联、锁粗化和循环展开。

每个实现都受到 4 种不同的线程场景的影响,这些场景会导致不同的争用配置文件:

  • 1 位读者 - 1 位作者
  • 2 位读者 - 1 位作者
  • 3 位读者 - 1 位作者
  • 2 位读者 - 2 位作者

所有测试均使用 64 位 Java 1.7.0_25、Linux 3.6.30 和四核 2.2GHz Ivy Bridge i7-3632QM 运行。对于每个实施,吞吐量在 5 秒内测量,测试重复 5 次以确保充分预热。下面的结果是 5 次运行的平均每秒吞吐量。为了近似典型的 Java 部署,没有使用线程亲和性或核心隔离,这会显着减少方差。

注意:其他 CPU 和操作系统可能会产生非常不同的结果。

结果

img
图1。
img
图 2。
img
图 3。
img
图 4。

上述图表的原始数据可在此处找到。

分析

结果让我真正吃惊的是 ReentrantReadWriteLock 的性能。除了在读取和写入很少的情况下存在巨大平衡的情况之外,我看不到这种实现的用途。我的主要收获是:

  1. StampedLock 是对现有锁实现的重大改进,尤其是随着阅读器线程数量的增加。
  2. StampedLock 有一个复杂的 API。很容易错误地调用错误的锁定操作方法。
  3. 当争用仅来自 2 个线程时,同步是一个很好的通用锁实现。
  4. ReentrantLock 是一个很好的通用锁实现,当线程数如先前发现的那样增长时。
  5. 选择使用 ReentrantReadWriteLock 应该基于仔细和适当的测量。与所有重大决策一样,根据数据衡量和做出决策。
  6. 与基于锁的算法相比,无锁实现可以提供显着的吞吐量优势。

结论

很高兴看到基于锁的算法中出现的无锁技术的影响。在写入器不更新时,读取时采用的乐观策略实际上是一种无锁算法。


文章作者: Kevin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kevin !
评论
  目录