为什么这个代码在锁定时运行得更快?

时间:2015-02-24 23:01:26

标签: java performance

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

请考虑以下代码(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%左右,在核心上相对均匀地分布。

第二个出人意料的是,这也取决于所使用的generator的类型。如果我使用GaussianRandomGeneratorUniformRandomGenerator代替StableRandomGenerator,则会观察到预期结果 - 通过删除lock(),代码运行得更快(大约10%)。< / p>

如果线程没有阻塞,CPU处于合理的水平,并且没有涉及IO,如何解释?我真正有的唯一线索是StableRandomGenerator会调用很多三角函数,因此显然比高斯或统一生成器的CPU密集程度要高得多,但为什么我没有看到CPU被最大化?

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

EDIT2:虽然这个问题在表面上非常类似于链接的重复问题,但答案似乎是合理的,几乎可以肯定是一个因素,但我还是不相信它的问题。就这么简单。让我质疑的事情是:

1)问题仅通过同步multiply()操作来显示,该操作不会调用Random。我的直接想法是,同步最终会在一定程度上扰乱线程,因此意外地#34;提高了Random#next()的性能。但是,同步调用generator.nextVector()(在理论上具有相同的效果,以#34;正确的方式)不会重现问题 - 同步会降低代码的速度,如您所料。 / p>

2)问题仅在StableRandomGenerator时观察到,即使NormalizedRandomGenerator的其他实现也使用JDKRandomGenerator(正如指出的那样只是{{1} }})。实际上,我使用java.util.Random替换了对RandomVectorGenerator的直接调用填充矩阵,并且行为再次恢复到预期结果 - 同步代码的任何部分导致总吞吐量下降。

总之,

可以来解决问题

a)使用Random#nextDouble - 没有StableRandomGenerator的其他子类,也不直接使用NormalizedRandomGeneratorJDKRandomGenerator,显示相同的行为。

b)将呼叫同步到java.util.Random。同步调用随机生成器时,未观察到相同的行为。

2 个答案:

答案 0 :(得分:4)

here相同的问题。

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

JDKRandomGenerator基于java.util.Random,其中seed在所有工作线程中共享。线程竞争更新compare-and-set loop中的seed

为什么lock可以提高性能呢?实际上,通过序列化工作有助于减少java.util.Random内的争用:当一个线程执行矩阵乘法时,另一个线程用随机数填充矩阵。没有lock个线程同时执行相同的工作。

答案 1 :(得分:2)

使用随机数生成器时需要记住很多。长话短说,你的怪癖是因为发电机必须收集足够的熵才能给你一个随机数。通过共享生成器,每次调用都需要熵来补充备份,所以这是你的阻塞点。现在,有些生成器在处理熵方面的工作方式与其他生成器的工作方式不同,因此有些生成器更受影响或链,而不是从头开始构建。当您在实例中创建生成器时,每个实例都会自行构建熵,因此速度更快。

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

此外,对于使用java.util.Random的任何内容,来自Random

  

java.util.Random的实例是线程安全的。但是,跨线程并发使用相同的java.util.Random实例可能会遇到争用并因此导致性能不佳。请考虑在多线程设计中使用ThreadLocalRandom。

相关问题