为什么此代码使用锁运行得更快?

2022-09-04 22:54:14

一些背景:我创建了一个人为的示例,向我的团队演示 VisualVM 的使用。特别是,一个方法有一个不必要的关键字,我们看到线程池中的线程阻塞,它们不需要。但是删除该关键字具有下面描述的令人惊讶的效果,下面的代码是最简单的情况,我可以将原始示例简化为重现问题,并且使用a也会产生相同的效果。synchronizedReentrantLock

请考虑下面的代码(https://gist.github.com/revbingo/4c035aa29d3c7b50ed8b 的完整可运行代码示例 - 您需要将Commons Math 3.4.1添加到类路径中)。它创建 100 个任务,并将它们提交到包含 5 个线程的线程池。在任务中,将创建两个随机值的 500x500 矩阵,然后进行乘法。

public class Main {
private static ExecutorService exec = Executors.newFixedThreadPool(5);

private final static int MATRIX_SIZE = 500;
private static UncorrelatedRandomVectorGenerator generator = 
            new UncorrelatedRandomVectorGenerator(MATRIX_SIZE, new StableRandomGenerator(new JDKRandomGenerator(), 0.1d, 1.0d));

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws Exception {

    for(int i=0; i < 100; i++) {

        exec.execute(new Runnable() {
            @Override
            public void run() {
                double[][] matrixArrayA = new double[MATRIX_SIZE][MATRIX_SIZE];
                double[][] matrixArrayB = new double[MATRIX_SIZE][MATRIX_SIZE];
                for(int j = 0; j< MATRIX_SIZE; j++) {
                    matrixArrayA[j] = generator.nextVector();
                    matrixArrayB[j] = generator.nextVector();
                }

                RealMatrix matrixA = MatrixUtils.createRealMatrix(matrixArrayA);
                RealMatrix matrixB = MatrixUtils.createRealMatrix(matrixArrayB);

                lock.lock();
                matrixA.multiply(matrixB);
                lock.unlock();
            }
        });
    }
}
}

这实际上是不必要的。需要同步的线程之间没有共享状态。锁定就绪后,我们预期会观察到线程池中的线程阻塞。删除锁后,我们预计不会再观察到阻塞,并且所有线程都能够完全并行运行。ReentrantLock

删除锁的意外结果是,代码始终需要更长的时间才能完成,在我的机器(四核i7)上,代码需要15-25%。对代码的分析没有显示线程中存在任何阻塞或等待的迹象,并且总CPU使用率仅为50%左右,相对均匀地分布在内核上。

第二个意想不到的事情是,这也取决于所使用的类型。如果我使用 a 或 代替 ,则观察到预期的结果 - 通过删除 .generatorGaussianRandomGeneratorUniformRandomGeneratorStableRandomGeneratorlock()

如果线程没有阻塞,CPU处于合理的水平,并且不涉及IO,这如何解释?我真正拥有的唯一线索是,它确实调用了很多三角函数,所以显然比高斯或均匀生成器占用更多的CPU,但是为什么我没有看到CPU被最大化呢?StableRandomGenerator

编辑:另一个要点(感谢 Joop) - 使 Runnable 成为本地(即每个线程一个)显示正常的预期行为,其中添加锁会使代码速度降低约 50%。因此,奇数行为的关键条件是 a) 使用 a 和 b) 在线程之间共享该生成器。但据我所知,该生成器是线程安全的。generatorStableRandomGenerator

编辑2:虽然这个问题表面上与链接的重复问题非常相似,而且答案是合理的,几乎可以肯定是一个因素,但我还没有确信它就这么简单。让我质疑它的事情:

1)问题仅通过同步操作显示,该操作不会对进行任何调用。我立即想到的是,这种同步最终会在某种程度上错开线程,因此“意外地”提高了 .但是,在调用上进行同步(理论上以“正确”的方式具有相同的效果)不会重现问题 - 同步会像您预期的那样减慢代码的速度。multiply()RandomRandom#next()generator.nextVector()

2) 问题只用 ,即使 的其他实现也使用 the (正如所指出的只是一个包装 )。事实上,我用直接调用 取代了 对 矩阵的填充,并且行为再次恢复到预期的结果 - 同步代码的任何部分都会导致总吞吐量下降。StableRandomGeneratorNormalizedRandomGeneratorJDKRandomGeneratorjava.util.RandomRandomVectorGeneratorRandom#nextDouble

综上所述,问题只能通过以下方式观察:

a) 使用 - 没有其他子类,也没有使用或直接显示相同的行为。StableRandomGeneratorNormalizedRandomGeneratorJDKRandomGeneratorjava.util.Random

b) 同步对 的调用。在同步对随机生成器的调用时,不会观察到相同的行为。RealMatrix#multiply


答案 1

此处相同的问题。

您实际上是在测量具有共享状态的 PRNG 内部的争用。

JDKRandomGenerator基于在所有工作线程之间共享的内容。线程在比较和设置循环中争用更新。java.util.Randomseedseed

那么,为什么要提高性能呢?事实上,它有助于通过序列化工作来减少内部的争用:一个线程执行矩阵乘法,另一个线程用随机数填充矩阵。如果没有线程,则同时执行相同的工作。lockjava.util.Randomlock


答案 2

使用随机数生成器时,需要记住很多内容。长话短说,你的怪癖是因为生成器必须收集足够的熵才能给你一个随机数。通过共享生成器,每个调用都需要熵来“填充备份”,因此它是您的阻塞点。现在,一些生成器在如何收集熵方面的工作方式与其他生成器不同,因此有些生成器的影响更大或更连锁,而不是从头开始构建。当您在实例中生成生成器时,每个实例都会自行构建熵,因此速度更快。

让我向你指出SecureRandom,特别是JavaDoc类,它说:“注意:根据实现,generateSeed和nextBytes方法可能会在收集熵时阻塞,例如,如果它们需要在各种类unix操作系统上从/dev/random读取。这就是你所看到的,也是事情进展缓慢的原因。使用单个生成器,它不断阻塞。是的,它是线程安全的,但它在获取熵时会阻塞(请注意,当线程等待阻塞方法从生成构建熵的随机数等返回时,线程中存在争用)。当你把自己的锁放进去时,你是在给它时间收集熵,并以“礼貌”的方式做它的事情。它可能是线程安全的,但这并不意味着它在轰炸时是好的或有效的;-)

另外,对于任何使用java.util.Random的东西,从Random

java.util.Random 的实例是 threadsafe。但是,跨线程并发使用相同的 java.util.Random 实例可能会遇到争用,从而导致性能下降。请考虑在多线程设计中使用 ThreadLocalRandom。


推荐