一些背景: 我创建了一个人为的例子,以向我的团队展示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%)。< / 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
的其他子类,也不直接使用NormalizedRandomGenerator
或JDKRandomGenerator
,显示相同的行为。
b)将呼叫同步到java.util.Random
。同步调用随机生成器时,未观察到相同的行为。
答案 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。