Я создал еще один тест. На этот раз я протестировал различные способы синхронизации небольшого кода с использованием взаимного исключения из этого кода.
Код для защиты будет очень простым. Это простой счетчик:
//Init
int counter = 0;
//Critical section
counter++;
Критическая секция, если она не защищена системой синхронизации, не будет работать должным образом из-за возможных чередований (прочитайте статью о синхронизации, если вы не знаете, что такое чередование).
Я использовал три разных синхронизатора для синхронизации этого приращения:
- синхронизированный блок
- Семафоры (справедливые и нечестные)
- Явные блокировки (справедливые и несправедливые)
Я также использовал третий способ решения проблемы с 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, чтобы облегчить тестирование и синхронизацию различных механизмов.
Тест проводится в два этапа:
- Испытание только с одной нитью со сложной структурой тестов. Это также действует как разминка для другого кода.
- Тест с несколькими потоками (несколько тестов с увеличением количества потоков). Тест сделан с использованием небольшого кода, который я написал для этого случая. Каждый метод выполняется 2? 3 раза (8388608 раз точно).
Исходный код доступен в конце поста.
Тест был запущен на Ubuntu 10.04 с виртуальной машиной Java 6. Компьютер имеет 64-битный процессор Core 2 Duo 3,16 ГГц и 6Go DDR2.
Итак, давайте посмотрим на результаты. Сначала с одной нитью:
Первое, что мы видим, это то, что AtomicInteger — самая быстрая версия. Это связано с тем, что AtomicInteger не использует операцию ожидания, поэтому это приводит к меньшему количеству переключений контекста и большей производительности. Но это не совсем так в тесте, поэтому давайте сосредоточимся на 5 других методах. Мы видим, что синхронизированный метод самый быстрый и что честные методы немного медленнее, чем несправедливые, но не намного.
Теперь мы проверим масштабируемость всех этих методов, используя несколько потоков.
В этом методе мы видим, что честные методы ужасно медленны по сравнению с недобросовестными версиями. Действительно, добавление справедливости в синхронизатор действительно тяжело. Когда справедливо, потоки получают блокировки в порядке, который они просят. При несправедливых замках баржи разрешены. Поэтому, когда поток пытается получить блокировку и сделать ее доступной, он может получить ее, даже если есть потоки, отказывающиеся от блокировки. Добросовестность труднее обеспечить, потому что контекстных переключателей намного больше. Проблема не была здесь только с одним потоком, потому что это всегда справедливо.
Результаты для других версий такие же, как с одним потоком.
Давайте добавим еще две темы:
Честные версии становятся все медленнее, когда мы добавляем темы. Масштабируемость этих методов действительно плохая. Давайте посмотрим на график без честных версий:
На этот раз мы можем увидеть некоторые различия. Синхронизированный метод на этот раз медленнее, и у семафора есть небольшое преимущество. Давайте посмотрим с 8 потоков:
Здесь синхронизированный метод намного медленнее, чем другие методы. Похоже, что алгоритм синхронизированного блока менее масштабируем, чем явные блокировки и версии семафоров. Давайте посмотрим, что происходит с другим количеством потоков:
Я также сделал тест с другим количеством потоков (16, 64 и 256), но результаты такие же, как и у других.
По результатам можно сделать несколько выводов:
- Честные версии медленные. Если вам абсолютно не нужна справедливость, не используйте честные замки или семафоры
- Семафоры и явные блокировки имеют одинаковую производительность. Это связано с тем, что 2 класса (Semaphore и ReentrantLock) основаны на одном классе AbstractQueueSynchronizer, который используется почти всеми механизмами синхронизации Java
- Явные блокировки и семафоры более масштабируемы, чем синхронизированные блоки. Но это зависит от виртуальной машины, я видел другие результаты, указывающие, что разница намного меньше
- AtomicInteger — самый эффективный метод. Этот класс не обеспечивает взаимное исключение, но предоставляет поточно-ориентированные методы для работы с простыми значениями (есть версия для Long, Double, Boolean и даже Reference)
Вот и все для этого теста. Надеюсь, вам было интересно.
Источники бенчмарка: источники синхронизации синхронизации
С http://www.baptiste-wicht.com/2010/09/java-synchronization-mutual-exclusion-benchmark/