一些背景信息:我创建了一个人为的示例,向我的团队演示如何使用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) 让该 generator 在线程之间共享。但据我所知,该 generator 是线程安全的。
编辑2:虽然这个问题表面上非常类似于链接的重复问题,而且答案是合理的,并且几乎肯定是一个因素,但我还没有被说服它并不像那么简单。让我质疑它的东西:
1) 问题仅在同步 multiply() 操作时显示出来,该操作没有调用 Random。我的第一反应是同步在某种程度上使线程错开了,因此“意外地”改善了 Random#next() 的性能。然而,同步对对 generator.nextVector() 的调用(在理论上具有相同的效果,以“正确”的方式)不会复制该问题-同步会使代码变慢,如您所预期的那样。
2) 只有 StableRandomGenerator 才观察到该问题,即使其他 NormalizedRandomGenerator 的实现也使用 JDKRandomGenerator(如指出的只是 java.util.Random 的包装)。事实上,我用直接调用 Random#nextDouble 来填充矩阵代替了 RandomVectorGenerator 的使用,行为再次恢复到预期结果-同步代码的任何部分都会导致总吞吐量下降。
总之,该问题只能通过以下方式观察到:
a) 使用 StableRandomGenerator-任何 NormalizedRandomGenerator 的子类或直接使用 JDKRandomGenerator 或 java.util.Random 都不显示相同的行为。
b) 同步对 RealMatrix#multiply 的调用。当同步随机生成器的调用时,不会观察到相同的行为。
generator
不是静态变量而是局部变量时会发生什么? - Joop Eggen