Некоторые предпосылки: Я создал надуманный пример, чтобы продемонстрировать использование 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%. Профилирование кода не показывает каких-либо блокировок или ожиданий в потоках, а общий объем использования ЦП составляет всего около 50%, распределенных относительно равномерно по ядрам.
Вторая неожиданная вещь заключается в том, что это также зависит от типа generator
, который используется. Если я использую GaussianRandomGenerator
или UniformRandomGenerator
вместо StableRandomGenerator
, ожидаемый результат наблюдается - код работает быстрее (примерно на 10%), удаляя lock()
.
Если потоки не блокируются, ЦП находится на разумном уровне, и в нем не участвует ИО, как это можно объяснить? Единственный ключ, который у меня есть, состоит в том, что StableRandomGenerator
вызывает много тригонометрических функций, поэтому явно намного интенсивнее процессор, чем генераторы Гаусса или Uniform, но почему тогда я не вижу, чтобы CPU был максимальным?
EDIT: Еще один важный момент (благодаря Joop) - создание generator
локального для Runnable (т.е. по одному на поток) отображает нормальное ожидаемое поведение, когда добавление блокировки замедляет код примерно на 50 %. Таким образом, ключевыми условиями для нечетного поведения являются: a) использование StableRandomGenerator
и b), когда этот генератор делится между потоками. Но, насколько мне известно, этот генератор является потокобезопасным.
EDIT2: Хотя этот вопрос поверхностно очень похож на связанный дублированный вопрос, а ответ правдоподобен и почти наверняка является фактором, я еще не убежден, что он достаточно прост. Вещи, которые заставляют меня подвергать сомнению:
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
. Такое же поведение не наблюдается при синхронизации вызовов с случайным генератором.