Skip to content

yozh1k/lockbenchmark

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Synchronized vs Lock

При обстоятельствах, детали которых не так важны, был сформулирован следующий тезис. Использование ключевого слова synchronized дает выигрыш в производительности по сравнению с java.util.concurrent.locks.Lock.

Мне показалось это утверждение спорным и я решил проверить его. Я нашел статьи датированные 2011 годом, авторы которых были солидарны со мной, но на дворе 2020 год и я решил проверить на практике, можно ли по крайней мере однозначно соглашаться или не соглашаться с озвученным утверждением.

Для этого я написал простейший потоконебезопасный счетчик:

public class NonThreadSafeCounter {
    private volatile int i = 0;
    public void increment() {
        i++;
    }
}

Для тестирования я подготовил 3 способа синхронного вызова счетчика:

  • с использованием synchronized-секции
    private void doMeasure() {
        synchronized (lock) {
            counter.increment();
        }
    }
  • с использованием synchronized-блока
    private class SynchronizedCounterWrapper {
        private synchronized void doMeasure() {
            counter.increment();
        }
    }
  • с использованием ReentrantLock'а
   private ReentrantLock lock;
   private NonThreadSafeCounter counter;
   private void doMeasure() {
       lock.lock();
       counter.increment();
       lock.unlock();
   }

Затем для каждого из способов синхронизации при помощи фреймворка jmh я создал 3 метода-бенчмарка, которые отличались количеством потоков, а именно: однопоточный вариант, для 2х потоков и для количества потоков, эквивалентного числу доступных ядер.

Итоговый бенчмарк для ReentrantLock я приведу ниже, остальные сделаны аналогично.

@State(Scope.Benchmark)
public class LockBenchmark {
    private ReentrantLock lock;
    private NonThreadSafeCounter counter;

    @Setup(Level.Iteration)
    public void setUpCounter() {
        lock = new ReentrantLock();
        counter = new NonThreadSafeCounter();
    }

    private void doMeasure() {
        lock.lock();
        counter.increment();
        lock.unlock();
    }

    @Threads(1)
    @Benchmark
    public void singleThreadMeasure() {
        doMeasure();
    }

    @Threads(2)
    @Benchmark
    public void twoThreadMeasure() {
        doMeasure();
    }

    @Threads(8)
    @Benchmark
    public void coreCountThreadMeasure() {
        doMeasure();
    }
}

Затем я прогнал получившиеся бенчмарки на своем компьютере. Конфигурация тестового окружения: Linux Mint 19 Cinnamon 3.8.9, Intel Core i7-3770 CPU @ 3.40GHz × 4

В качестве JVM я использовал JDK 1.8.0_252. И получил следующие результаты для количества операций в секунду.

LockBenchmark SynchronizedMethodBenchmark SynchronizedSectionBenchmark
coreCountThreadMeasure 13139774,377 9233990,876 8895153,208
singleThreadMeasure 49469746,932 94795184,779 137784882,204
twoThreadMeasure 7955459,563 15201228,719 15421543,665

В виде накопительной гистрограммы это выглядит так:

picture

Результаты получились неоднозначные. Действительно, для небольшого количества конкурирующих потоков или вовсе в отсутствие конкуреции synchronized работает быстрее. Причем разница в способе использования synchronized для однопоточного варианта оказывается довольно существенной. Однако при увеличении числа конкурирующих потоков оказывается вперёд уже вырывается ReentrantLock.

Я дополнительно прогнал те же измерения с использованием других версий JDK.

Результаты 11 версии

LockBenchmark SynchronizedMethodBenchmark SynchronizedSectionBenchmark
coreCountThreadMeasure 12687540,493 9387418,715 9893503,33
singleThreadMeasure 48614869,449 138317014,948 137726815,486
twoThreadMeasure 8153936,961 22756867,238 24366107,333

picture Здесь результаты примерно те же за исключением того, что разница при различных способах использования synchronized практически ичезает.

Результаты 13 версии

LockBenchmark SynchronizedMethodBenchmark SynchronizedSectionBenchmark
coreCountThreadMeasure 11920392,211 9381171,059 9751548,69
singleThreadMeasure 48648991,035 138401909,855 138389513,615
twoThreadMeasure 7975837,493 28299790,093 27811727,651

picture

Опять же, результаты примерно те же, но преимущество в скорости при использовании Lock с большим количеством потоков немного сокращается

Результаты 14 версии

LockBenchmark SynchronizedMethodBenchmark SynchronizedSectionBenchmark
coreCountThreadMeasure 12914538,781 9799225,132 9803710,055
singleThreadMeasure 46969083,489 136512156,589 136539311,766
twoThreadMeasure 7411408,252 28430453,047 27179498,132

picture

В этом случае результаты практически такие же, как и у 13й версии

Таким образом, независимо от весрии jdk результаты были приблизительно аналогичными. Причины такого поведения кроются в том, что JDK использует 4 типа локов [https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/thread_basics.html]:

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

При этом толстые локи могут со временем превращаться в тонкие и наоборот. Очевидно, для различных подходов к синхронизации используются различные алгоритмы и комбинации локов, что ведет к различному поведению. В блоге Руслана Черемина(https://dev.cheremin.info) есть несколько статей, которые пытаются пролить на это свет.

В качестве резюме я бы сформулировал следующее утверждение: исследование выполненно довольно поверхностно и не претендует на истину в последней инстанции, но очевидно показывает, что утверждать, что использование synchronized в общем случае предпочтительнее с точки зрения производительности, чем java.util.concurrent.locks.Lock, нельзя.

Исходный код и результаты доступны по адресу: https://github.com/yozh1k/lockbenchmark

Запусить тесты можно перейдя в корень проекта:

mvn clean install
java -jar target/benchmarks.jar

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages