Существуют разные методы многопоточности в 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:
Для размера пула 4 и количества потоков 4:
Для размера пула 6 и количества потоков 6:
Для размера пула 8 и количества потоков 8:
Для размера пула 10 и количества потоков 10:
Для размера пула 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