Статьи

Тест синхронизации Java (взаимное исключение)

Я создал еще один тест. На этот раз я протестировал различные способы синхронизации небольшого кода с использованием взаимного исключения из этого кода.

Код для защиты будет очень простым. Это простой счетчик:

//Init
int counter = 0;
 
//Critical section
counter++;

Критическая секция, если она не защищена системой синхронизации, не будет работать должным образом из-за возможных чередований (прочитайте статью о синхронизации, если вы не знаете, что такое чередование).

Я использовал три разных синхронизатора для синхронизации этого приращения:

  1. синхронизированный блок
  2. Семафоры (справедливые и нечестные)
  3. Явные блокировки (справедливые и несправедливые)

Я также использовал третий способ решения проблемы с AtomicInteger. Это не то же самое, что другие способы, потому что это не обеспечивает взаимное исключение. Это хороший способ синхронизировать простые значения, такие как целые или логические, а также ссылки. Атомарность операций AtomicInteger выполняется с помощью операции сравнения и замены операционной системы. Так что нет никаких операций ожидания. Это означает, что у нас меньше переключений контекста и обычно получается более производительный код.

Вот код этих 4 способов решить проблемы:

private class SynchronizedRunnable implements Runnable {
private int counter = 0;
 
@Override
public synchronized void run() {
counter++;
}
}
 
private class ReentrantLockRunnable implements Runnable {
private int counter = 0;
 
private Lock lock;
 
private ReentrantLockRunnable(boolean fair) {
super();
 
lock = new ReentrantLock(fair);
}
 
@Override
public void run() {
lock.lock();
 
try {
counter++;
} finally {
lock.unlock();
}
}
}
 
private class SemaphoreRunnable implements Runnable {
private int counter = 0;
 
private final Semaphore semaphore;
 
private SemaphoreRunnable(boolean fair) {
super();
 
semaphore = new Semaphore(1, fair);
}
 
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
 
try {
counter++;
} finally {
semaphore.release();
}
}
}
 
private class AtomicIntegerRunnable implements Runnable {
private AtomicInteger counter = new AtomicInteger(0);
 
@Override
public void run() {
counter.incrementAndGet();
}
}

Я использовал Runnable, чтобы облегчить тестирование и синхронизацию различных механизмов.

Тест проводится в два этапа:

  1. Испытание только с одной нитью со сложной структурой тестов. Это также действует как разминка для другого кода.
  2. Тест с несколькими потоками (несколько тестов с увеличением количества потоков). Тест сделан с использованием небольшого кода, который я написал для этого случая. Каждый метод выполняется 2? 3 раза (8388608 раз точно).

Исходный код доступен в конце поста.

Тест был запущен на Ubuntu 10.04 с виртуальной машиной Java 6. Компьютер имеет 64-битный процессор Core 2 Duo 3,16 ГГц и 6Go DDR2.

Итак, давайте посмотрим на результаты. Сначала с одной нитью:

Тест синхронизации - один поток

Тест синхронизации — один поток

Первое, что мы видим, это то, что AtomicInteger — самая быстрая версия. Это связано с тем, что AtomicInteger не использует операцию ожидания, поэтому это приводит к меньшему количеству переключений контекста и большей производительности. Но это не совсем так в тесте, поэтому давайте сосредоточимся на 5 других методах. Мы видим, что синхронизированный метод самый быстрый и что честные методы немного медленнее, чем несправедливые, но не намного.

Теперь мы проверим масштабируемость всех этих методов, используя несколько потоков.

Синхронизация - 2 потока

Синхронизация — 2 потока

В этом методе мы видим, что честные методы ужасно медленны по сравнению с недобросовестными версиями. Действительно, добавление справедливости в синхронизатор действительно тяжело. Когда справедливо, потоки получают блокировки в порядке, который они просят. При несправедливых замках баржи разрешены. Поэтому, когда поток пытается получить блокировку и сделать ее доступной, он может получить ее, даже если есть потоки, отказывающиеся от блокировки. Добросовестность труднее обеспечить, потому что контекстных переключателей намного больше. Проблема не была здесь только с одним потоком, потому что это всегда справедливо.

Результаты для других версий такие же, как с одним потоком.

Давайте добавим еще две темы:

Синхронизация - 4 потока

Синхронизация — 4 потока

Честные версии становятся все медленнее, когда мы добавляем темы. Масштабируемость этих методов действительно плохая. Давайте посмотрим на график без честных версий:

Синхронизация - 4 потока

Синхронизация — 4 потока

На этот раз мы можем увидеть некоторые различия. Синхронизированный метод на этот раз медленнее, и у семафора есть небольшое преимущество. Давайте посмотрим с 8 потоков:

Синхронизация - 8 потоков

Синхронизация — 8 потоков

Здесь синхронизированный метод намного медленнее, чем другие методы. Похоже, что алгоритм синхронизированного блока менее масштабируем, чем явные блокировки и версии семафоров. Давайте посмотрим, что происходит с другим количеством потоков:

Синхронизация - 32 потока

Синхронизация — 32 потока

Синхронизация - 128 потоков

Синхронизация — 128 потоков

Я также сделал тест с другим количеством потоков (16, 64 и 256), но результаты такие же, как и у других.

По результатам можно сделать несколько выводов:

  1. Честные версии медленные. Если вам абсолютно не нужна справедливость, не используйте честные замки или семафоры
  2. Семафоры и явные блокировки имеют одинаковую производительность. Это связано с тем, что 2 класса (Semaphore и ReentrantLock) основаны на одном классе AbstractQueueSynchronizer, который используется почти всеми механизмами синхронизации Java
  3. Явные блокировки и семафоры более масштабируемы, чем синхронизированные блоки. Но это зависит от виртуальной машины, я видел другие результаты, указывающие, что разница намного меньше
  4. AtomicInteger — самый эффективный метод. Этот класс не обеспечивает взаимное исключение, но предоставляет поточно-ориентированные методы для работы с простыми значениями (есть версия для Long, Double, Boolean и даже Reference)

Вот и все для этого теста. Надеюсь, вам было интересно.

Источники бенчмарка:  источники синхронизации синхронизации

С http://www.baptiste-wicht.com/2010/09/java-synchronization-mutual-exclusion-benchmark/