Статьи

Сравнение производительности многопоточности в Java

Существуют разные методы многопоточности в Java. Можно распараллелить кусок кода в Java либо синхронизировать ключевые слова, блокировки или атомарные переменные. Этот пост будет сравнивать эффективность использования синхронизированного ключевого слова, ReentrantLock, getAndIncrement () и выполнения непрерывных испытаний вызовов get () и compareAndSet (). Для тестирования производительности созданы различные типы классов Matrix, в том числе и простой. Для сравнения, все ячейки увеличились в 100 раз для разных размеров матриц, с разными типами синхронизации, количеством потоков и размерами пула на компьютере с процессором Intel Core I7 (имеет 8 ядер — 4 из них действительные), Ubuntu 14.04 LTS и Java 1.7.0_60.

Это класс теста производительности в виде простой матрицы:

/**
* Plain matrix without synchronization.
*/
public class Matrix {
private int rows;
private int cols;
private int[][] array;
/**
* Matrix constructor.
*
* @param rows number of rows
* @param cols number of columns
*/
public Matrix(int rows, int cols) {
this.rows = rows;
this.cols = cols;
array = new int[rows][rows];
}
/**
* Increments all matrix cells.
*/
public void increment() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
array[i][j]++;
}
}
}
/**
* Returns a string representation of the object which shows row sums of each row.
*
* @return a string representation of the object.
*/
@Override
public String toString() {
StringBuffer s = new StringBuffer();
int rowSum;
for (int i = 0; i < rows; i++) {
rowSum = 0;
for (int j = 0; j < cols; j++) {
rowSum += array[i][j];
}
s.append(rowSum);
s.append(" ");
}
return s.toString();
}
}

Для других,   методы приращения их перечислены, потому что остальные части одинаковы для каждого типа матрицы.

Синхронизированная матрица:

public void increment() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
synchronized (this) {
array[i][j]++;
}
}
}
}

Блокировка матрицы:

public void increment() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
lock.lock();
try {
array[i][j]++;
} finally {
lock.unlock();
}
}
}
}

Атомная матрица getAndIncrement:

public void increment() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
array[i][j].getAndIncrement();
}
}
}

Непрерывные испытания матрицы get () и compareAndSet ():

public void increment() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
for (; ; ) {
int current = array[i][j].get();
int next = current + 1;
if (array[i][j].compareAndSet(current, next)) {
break;
}
}
}
}
}

Также рабочие классы создаются для каждой матрицы. Вот рабочий класс равнины:

/**
* Worker for plain matrix without synchronization.
*
* @author Furkan KAMACI
* @see Matrix
*/
public class PlainMatrixWorker extends Matrix implements Runnable {
private AtomicInteger incrementCount = new AtomicInteger(WorkerDefaults.INCREMENT_COUNT);
/**
* Worker constructor.
*
* @param rows number of rows
* @param cols number of columns
*/
public PlainMatrixWorker(int rows, int cols) {
super(rows, cols);
}
/**
* Increments matrix up to a maximum number.
*
* @see WorkerDefaults
*/
@Override
public void run() {
while (incrementCount.getAndDecrement() > 0) {
increment();
}
}
}

Для правильного сравнения на все тесты по умолчанию отвечают 20 раз. Средние и стандартные ошибки рассчитываются для каждого результата. Из-за большого количества измерений в наборе тестов (тип матрицы, размер матрицы, размер пула, количество потоков и истекшее время) некоторые функции отображаются в виде агрегированных данных на диаграммах. Вот результаты:

Для размера пула 2 и количества потоков 2:

Размер пула 2 - Количество нитей 2

Для размера пула 4 и количества потоков 4:

Размер бассейна 4 - Количество нитей 4

Для размера пула 6 и количества потоков 6:

Размер пула 6 - Количество нитей 6

Для размера пула 8 и количества потоков 8:

Размер пула 8 - Количество нитей 8

Для размера пула 10 и количества потоков 10:

Размер пула 10 - Количество нитей 10

Для размера пула 12 и количества нитей 12:

Размер пула 12 - Количество нитей 12

Вывод:

Легко увидеть, что простая версия работает быстрее всего. Однако это не дает правильных результатов, как ожидалось. Хуже производительность видна с синхронизированными блоками (когда синхронизация выполняется с « этим »). Замки немного лучше, чем синхронизированные блоки. Однако атомные переменные заметно лучше всех. При сравнении атомарных getAndIncrement и непрерывных испытаний вызовов get () и compareAndSet () показано, что их производительность одинакова. Причину этого легко понять, когда проверен исходный код Java:

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}

Можно видеть, что getAndIncrement реализован с помощью непрерывных испытаний get () и compareAndSet () в исходном коде Java (версия 1.7). С другой стороны, когда проверяются другие результаты, можно увидеть влияние размера пула. При использовании размера пула, который меньше фактического значения потока, может возникнуть проблема с производительностью.

Таким образом, сравнение производительности многопоточности в Java показывает, что когда часть кода решается синхронизировать и производительность является проблемой, и если потоки такого типа будут использоваться, как в тесте, следует попытаться использовать атомарные переменные. Другими вариантами должны быть блокировки или синхронизированные блоки. Также это не означает, что синхронизированные блоки всегда лучше блокировок из-за эффекта JIT-компилятора и запуска фрагмента кода несколько раз или нет.

Исходный код для сравнения производительности многопоточности в Java можно скачать здесь:  https://github.com/kamaci/performance