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

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

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

 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% lock()

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

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

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

1)只能通过同步multiply()操作来显示问题,该操作不会对Random进行任何调用。 我的直接想法是,同步最终会在某种程度上错开线程,因此“意外地”改善了Random#next()的性能。 但是,同步对generator.nextVector()的调用generator.nextVector()理论上它以“正确”的方式具有相同的效果)不会重现问题 – 同步会降低代码的速度,如您所料。

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

总之,这个问题只能通过以下方式来观察

a)使用StableRandomGenerator – 没有NormalizedRandomGenerator其他子类,也不直接使用JDKRandomGeneratorjava.util.Random ,显示相同的行为。

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

和这里一样的问题。

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

JDKRandomGenerator基于java.util.Random ,它在所有工作线程之间共享seed 。 线程竞争在比较和设置循环中更新seed

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

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

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

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

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