在Java中,AtomicInteger compareAndSet()与synced关键字的性能如何?

2022-09-01 15:47:49

我正在实现请求实例的FIFO队列(为了速度而预先分配的请求对象),并开始在add方法上使用“synced”关键字。该方法非常短(检查空间是否在固定大小的缓冲区中,然后向数组添加值)。使用visualVM,线程似乎比我喜欢的更频繁地阻塞(确切地说是“监视器”)。因此,我将代码转换为使用AtomicInteger值来跟踪当前大小,然后在while循环中使用compareAndSet()(如AtomicInteger在内部对增量AndGet()等方法所做的那样)。代码现在看起来更长了。

我想知道的是,使用同步和较短的代码与不使用synced关键字的较长代码相比,性能开销是多少(因此永远不应该在锁定上阻塞)。

下面是带有 sync 关键字的旧 get 方法:

public synchronized Request get()
{
    if (head == tail)
    {
        return null;
    }
    Request r = requests[head];
    head = (head + 1) % requests.length;
    return r;
}

下面是没有 sync 关键字的新 get 方法:

public Request get()
{
    while (true)
    {
        int current = size.get();
        if (current <= 0)
        {
            return null;
        }
        if (size.compareAndSet(current, current - 1))
        {
            break;
        }
    }

    while (true)
    {
        int current = head.get();
        int nextHead = (current + 1) % requests.length;
        if (head.compareAndSet(current, nextHead))
        {
            return requests[current];
        }
    }
}

我的猜测是,同步关键字更糟糕,因为锁定上有阻塞的风险(可能导致线程上下文切换等),即使代码更短。

谢谢!


答案 1

我的猜测是同步关键字更糟,因为锁定有阻塞的风险(可能导致线程上下文切换等)

是的,在常见情况下,你是对的。Java 并发实践在第 15.3.2 节中对此进行了讨论:

[...]在高争用级别,锁定往往优于原子变量,但在更现实的争用级别,原子变量的性能优于锁定。这是因为锁通过挂起线程、减少共享内存总线上的 CPU 使用率和同步流量来对争用做出反应(这类似于在生产者-消费者设计中阻止生产者如何减少消费者的负载,从而让他们赶上来。另一方面,对于原子变量,争用管理被推回调用类。与大多数基于 CAS 的算法一样,通过立即重试来对争用做出反应,这通常是正确的方法,但在高争用环境中只会产生更多的争用。AtomicPseudoRandom

在我们谴责与锁相比,编写不佳或原子变量是一个糟糕的选择之前,我们应该意识到图15.1中的争用水平是不切实际的:没有一个真正的程序除了争夺锁或原子变量之外什么都不做。在实践中,原子键的扩展性往往比锁的好,因为原子键可以更有效地处理典型的争用级别。AtomicPseudoRandom

在不同争用级别下,锁和原子之间的性能反转说明了每种锁和原子的优缺点。对于低到中等的争用,原子能提供更好的可扩展性;对于高争用,锁可提供更好的争用避免。(在单 CPU 系统上,基于 CAS 的算法的性能也优于基于锁的算法,因为 CAS 在单 CPU 系统上始终成功,除非在读-修改-写入操作的中间抢占线程。

(在文本引用的数字上,图15.1显示,当争用率很高时,AtomicInteger和ReentrantLock的性能或多或少相等,而图15.2显示,在中等争用下,前者比后者高出2-3倍。

更新:关于非阻塞算法

正如其他人所指出的那样,非阻塞算法虽然可能更快,但更复杂,因此更难正确。JCiA 第 15.4 节中的提示:

良好的非阻塞算法对于许多常见的数据结构(包括堆栈,队列,优先级队列和哈希表)是众所周知的,尽管设计新的算法最好留给专家完成。

非阻塞算法比基于锁的等效算法复杂得多。创建非阻塞算法的关键是弄清楚如何将原子更改的范围限制为单个变量,同时保持数据一致性。在链接集合类(如队列)中,有时可以将状态转换表示为对各个链接的更改,并使用 a 来表示必须以原子方式更新的每个链接。AtomicReference


答案 2

我想知道jvm在真正挂起线程之前是否已经进行了几次旋转。它预计,像您这样写得很好的关键部分非常短,几乎可以立即完成。因此,它应该乐观地忙于等待,我不知道,几十个循环,然后放弃并暂停线程。如果是这种情况,它的行为应该与第二个版本相同。

分析器显示的内容可能与jvm中全速发生的真实情况有很大不同,并进行了各种疯狂的优化。最好在没有探查器的情况下测量和比较吞吐量。


推荐