Статьи

Реализации блокировки Java

Мы все используем сторонние библиотеки как обычную часть разработки. Как правило, мы не контролируем их внутренности. Библиотеки, поставляемые с JDK, являются типичным примером. Многие из этих библиотек используют блокировки для управления конфликтами.
Блокировки JDK поставляются с двумя реализациями. Один из них использует инструкции в стиле атомарного CAS для управления процессом подачи заявки. Инструкции CAS, как правило, являются самым дорогим типом инструкций процессора, а на x86 есть семантика упорядочения памяти . Часто блокировки неконтролируемы, что влечет за собой возможную оптимизацию, при которой блокировка может быть смещена к неконтролируемому потоку с использованием методов, позволяющих избежать использования атомарных инструкций. Такое смещение позволяет теоретически быстро восстановить блокировку одним и тем же потоком. Если блокировка оказывается состоящей из нескольких потоков, алгоритм возвращается из предвзятости и возвращается к стандартному подходу с использованием атомарных инструкций. Смещенная блокировка стала реализацией блокировки по умолчанию в Java 6.
При соблюдении принципа единственного писателя предвзятая блокировка должна быть вашим другом. В последнее время при использовании API сокетов я решил измерить стоимость блокировки и был удивлен результатами. Я обнаружил, что мой неконтролируемый поток несёт немного большую стоимость, чем я ожидал от блокировки. Я собрал следующий тест, чтобы сравнить стоимость текущих реализаций блокировки, доступных в Java 6.
Тест
Для теста я увеличу счетчик в замке и увеличу количество конкурирующих потоков в замке. Этот тест будет повторен для 3 основных реализаций блокировки, доступных для Java:
  1. Атомная блокировка на мониторах языка Java
  2. Предвзятая блокировка на мониторах языка Java
  3. ReentrantLock представлен в пакете java.util.concurrent в Java 5.
Я также проведу тесты на трех самых последних поколениях процессоров Intel. Для каждого процессора я буду выполнять тесты с максимальным количеством одновременных потоков, которое будет поддерживать число ядер.
Тесты проводятся с 64-битным Linux (Fedora Core 15) и Oracle JDK 1.6.0_29.

Код

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CyclicBarrier;
 
import static java.lang.System.out;
 
public final class TestLocks implements Runnable
{
    public enum LockType { JVM, JUC }
    public static LockType lockType;
 
    public static final long ITERATIONS = 500L * 1000L *1000L;
    public static long counter = 0L;
 
    public static final Object jvmLock = new Object();
    public static final Lock jucLock = new ReentrantLock();
    private static int numThreads;
    private static CyclicBarrier barrier;
 
    public static void main(final String[] args) throws Exception
    {
        lockType = LockType.valueOf(args[0]);
        numThreads = Integer.parseInt(args[1]);
         
        runTest(numThreads); // warm up
        counter = 0L;
 
        final long start = System.nanoTime();
        runTest(numThreads);
        final long duration = System.nanoTime() - start;
 
        out.printf("%d threads, duration %,d (ns)\n", numThreads, duration);
        out.printf("%,d ns/op\n", duration / ITERATIONS);
        out.printf("%,d ops/s\n", (ITERATIONS * 1000000000L) / duration);
        out.println("counter = " + counter);
    }
 
    private static void runTest(final int numThreads) throws Exception
    {
        barrier = new CyclicBarrier(numThreads);
        Thread[] threads = new Thread[numThreads];
 
        for (int i = 0; i < threads.length; i++)
        {
            threads[i] = new Thread(new TestLocks());
        }
 
        for (Thread t : threads)
        {
            t.start();
        }
 
        for (Thread t : threads)
        {
            t.join();
        }
    }
 
    public void run()
    {
        try
        {
            barrier.await();
        }
        catch (Exception e)
        {
            // don't care
        }
 
        switch (lockType)
        {
            case JVM: jvmLockInc(); break;
            case JUC: jucLockInc(); break;
        }
    }
 
    private void jvmLockInc()
    {
        long count = ITERATIONS / numThreads;
        while (0 != count--)
        {
            synchronized (jvmLock)
            {
                ++counter;
            }
        }
    }
 
    private void jucLockInc()
    {
        long count = ITERATIONS / numThreads;
        while (0 != count--)
        {
            jucLock.lock();
            try
            {
                ++counter;
            }
            finally
            {
                jucLock.unlock();
            }
        }
    }
}

Скрипт тестов:

установить -x
для меня в {1..8}; do java -XX: -UseBiasedLocking TestLocks JVM $ i; сделано
для меня в {1..8}; do java -XX: + UseBiasedLocking TestLocks JVM $ i; сделано
для меня в {1..8}; сделать Java TestLocks JUC $ я; сделано

Результаты

Рисунок 1
фигура 2
Рисунок 3
Смещенная блокировка больше не должна быть реализацией блокировки по умолчанию на современных процессорах Intel. Я рекомендую вам измерять свои приложения и эксперименты с опцией -XX: -UseBiasedLocking JVM, чтобы определить, можете ли вы извлечь выгоду из использования алгоритма атомарной блокировки для неконтролируемого случая.

наблюдения
  1. Смещенная блокировка в неконтролируемом случае на ~ 10% дороже, чем атомная блокировка Похоже, что для последних поколений процессоров стоимость атомарных инструкций меньше, чем необходимая служебная нагрузка для смещенных блокировок. До Nehalem инструкции блокировки устанавливали блокировку на шине памяти для выполнения этих атомарных операций, каждая из которых стоила бы более 100 циклов. Начиная с Nehalem, атомарные инструкции могут обрабатываться локально по отношению к ядру ЦП и, как правило, стоят всего 10-20 циклов, если им не нужно ждать, пока в буфере хранилища не будет опустошен при применении семантики упорядочения памяти.
  2. По мере роста конкуренции блокировка языкового монитора быстро достигает предела пропускной способности независимо от количества потоков.
  3. ReentrantLock обеспечивает лучшую безусловную производительность и значительно лучше масштабируется с ростом конкуренции по сравнению с языковыми мониторами, использующими синхронизированные.
  4. ReentrantLock имеет странную характеристику снижения производительности, когда конкурируют 2 потока. Это заслуживает дальнейшего изучения.
  5. Sandybridge страдает от увеличенной задержки атомарных инструкций, которые я подробно описывал в предыдущей статье, когда число предполагаемых потоков является низким. Поскольку число конкурирующих потоков продолжает расти, стоимость арбитража ядра становится доминирующей, и Sandybridge демонстрирует свою силу с увеличением пропускной способности памяти.
Вывод
При разработке собственных параллельных библиотек я бы порекомендовал ReentrantLock вместо использования синхронизированного ключевого слова из-за значительно лучшей производительности на x86, если альтернативный алгоритм без блокировки не является жизнеспособным вариантом.
Обновление 20 ноября 2011
Дейв Дайс отметил, что смещенная блокировка не реализована для блокировок, созданных в первые несколько секунд запуска JVM. Я проведу повторные тесты на этой неделе и опубликую результаты. У меня было еще несколько качественных отзывов о том, что мои результаты могут быть потенциально недействительными. Микро тесты могут быть хитрыми, но совет по измерению вашего собственного приложения в целом остается в силе.
Повторное тестирование можно увидеть в этом последующем блоге с учетом отзывов Дейва.
Ссылка: реализации Java Lock от нашего партнера JCG Мартина Томпсона в блоге Mechanical Sympathy .