一些背景:我创建了一个人为的示例来向我的团队演示 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
的类型使用的。如果我使用 GaussianRandomGenerator
或 UniformRandomGenerator
而不是 StableRandomGenerator
,观察到预期结果 - 通过删除 lock()
,代码运行速度更快(大约 10%) .如果线程不阻塞,CPU 处于合理水平,并且不涉及 IO,这如何解释?我真正掌握的唯一线索是
StableRandomGenerator
确实调用了很多三角函数,所以显然比高斯或均匀生成器更占用 CPU,但为什么我没有看到 CPU 被最大化?编辑:另一个重点(感谢 Joop) - 制作
generator
Runnable 本地(即每个线程一个)显示正常的预期行为,其中添加锁会使代码减慢约 50%。所以奇怪行为的关键条件是 a) 使用 StableRandomGenerator
, 和 b) 在线程之间共享该生成器。但据我所知,该生成器是线程安全的。编辑2:虽然这个问题在表面上与链接的重复问题非常相似,并且答案是合理的,而且几乎可以肯定是一个因素,但我还没有相信它就这么简单。让我质疑的事情:
1)问题只有在
multiply()
上同步才显示操作,不调用 Random
.我的直接想法是,同步最终会在某种程度上使线程错开,因此“意外地”提高了 Random#next()
的性能。 .但是,同步对 generator.nextVector()
的调用(理论上具有相同的效果,以“正确”的方式),不会重现该问题 - 如您所料,同步会减慢代码速度。2) 该问题仅在
StableRandomGenerator
上观察到,即使 NormalizedRandomGenerator
的其他实现也可以使用 JDKRandomGenerator
(正如所指出的,这只是 java.util.Random
的包装)。事实上,我替换了使用 RandomVectorGenerator
通过直接调用 Random#nextDouble
来填充矩阵,并且行为再次恢复到预期结果 - 同步代码的任何部分都会导致总吞吐量下降。综上所述,问题可以只有被观察到
a) 使用
StableRandomGenerator
- 没有其他 NormalizedRandomGenerator
的子类,也不使用 JDKRandomGenerator
或 java.util.Random
直接,显示相同的行为。b) 将调用同步到
RealMatrix#multiply
.将调用同步到随机生成器时,不会观察到相同的行为。
最佳答案
与here相同的问题.
您实际上是在测量具有共享状态的 PRNG 内的争用。JDKRandomGenerator
基于 java.util.Random
其中有 seed
在所有工作线程之间共享。线程竞争更新 seed
在 compare-and-set loop .
为什么lock
提高性能呢?事实上,它有助于减少内部争用 java.util.Random
通过序列化工作:当一个线程执行矩阵乘法时,另一个线程用随机数填充矩阵。无 lock
线程并发地做同样的工作。
关于java - 为什么这段代码在有锁的情况下运行得更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28708058/