ReentrantLock: скорость блокировки/разблокировки в однопоточном приложении

я использую ReentrantLock для синхронизации доступа к списку в нескольких потоках. Я просто пишу общий

try {
   lock.lock();
   ... modify list here
} finally {
   lock.unlock();
}

везде. Однако я только что заметил, что большинство списков в коде будут использоваться только одним потоком (отправление графического интерфейса пользователя).

Теперь я не уверен, должен ли я снимать блокировку в этих случаях, так как это может ускорить мой код. Насколько быстро работает ReentrantLock? Является ли операция блокировки () быстрее, если сам поток был «предыдущим владельцем» блокировки, даже если он разблокировал () ее?


person Fabian Zeindl    schedule 27.11.2011    source источник
comment
Почему бы вам не удалить его и не измерить разницу в производительности? Все остальное только догадки.   -  person skaffman    schedule 27.11.2011
comment
@skaffman: потому что я слышал, что микротесты легко сделать неправильно.   -  person Fabian Zeindl    schedule 27.11.2011
comment
Я не имею в виду микротесты. Я имею в виду использование приложения по-настоящему или под высокой нагрузкой. Если вы не видите разницы, то это ответ на ваш вопрос.   -  person skaffman    schedule 27.11.2011
comment
@sll, ReentrantLock в основном одинаков во всех версиях Java.   -  person bestsss    schedule 27.11.2011


Ответы (5)


Независимо от того, насколько это быстро, это определенно будет медленнее, чем без блокировки, хотя бы на очень небольшую величину. Лично я бы предпочел правильность приросту скорости (которого не будет много) и оставил бы код таким, какой он есть (особенно если он уже протестирован), если вы не определили, что «блокировка» является узким местом.

person Sanjay T. Sharma    schedule 27.11.2011

3 вещи:

  • lock.lock() должен находиться за пределами блока try.

  • если вы работаете на одноядерном процессоре, блокировка будет очень дешевой. Затем, в зависимости от архитектуры ЦП, приобретение/выпуск может быть дешевым или не очень. Nehalem+ вроде нормально.

  • если вам не нужны блокировки для чего-либо еще, synchronized может быть лучшим подходом, поскольку JVM может огрубить мониторы и/или сместить блокировку() в однопоточном приложении. Опять же, производительность смещенных блокировок сильно зависит от архитектуры процессора.

person bestsss    schedule 27.11.2011
comment
Почему lock() должен быть вне блока try? - person Fabian Zeindl; 28.11.2011
comment
@FabianZeindl, b/c, если произойдет сбой с исключением, вы должны выполнить unlock(), который также не сработает с IllegalMonitorStateException (или подобным). В конце концов вы получаете странное IllegalMonitorStateException из ниоткуда, теряя первоначальную причину. То же самое относится и ко всему, что нужно выпустить, это должно быть получено в первую очередь. - person bestsss; 28.11.2011

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

person Artefacto    schedule 27.11.2011
comment
установить и снять блокировку довольно недорого. правда, но/и не так сильно, это зависит от архитектуры процессора. - person bestsss; 27.11.2011

Я действительно не понимаю, является ли ваше приложение однопоточным или многопоточным. Название говорит одно, а по корпусу можно сделать вывод о другом. Я предполагаю, что вы используете более одного потока (иначе вопрос не имеет смысла — зачем вам использовать синхронизацию между... менее чем двумя потоками?!).

Там может быть более серьезная проблема, чем производительность. Видимость.

Вы не даете никаких подробностей о части кода, в которой вы ...modify a list..., однако очень важно знать подробности: в зависимости от того, как вы это сделали, если вы удалите блокировку, что может произойти: один поток изменяет список и другой поток никогда не увидят эти модификации (или увидят частичные, скорее всего, несогласованные модификации).

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

В этом случае вашу гарантию дают блокировки: когда поток T1 получает блокировку L, гарантируется, что он увидит все изменения, сделанные в памяти потоком T2 до того, как T2 освободит блокировку L.

   T2            T1
acquires L
modifies a
modifies b
releases L
modifies c    acquires L
              reads a
              reads b
              reads c
              releases L

В этом случае гарантируется, что T1 увидит правильные значения для a и b, но нет никакой гарантии, что он увидит при чтении c.

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

person Bruno Reis    schedule 27.11.2011
comment
Я использую несколько потоков, но при этом они не обмениваются данными. Я работаю с различными списками только в одном потоке. - person Fabian Zeindl; 28.11.2011
comment
@FabienZindl, ты никогда не трогал эти списки из других тем? Если это так, то вам вообще не нужна синхронизация (связанная с доступом к спискам). - person Bruno Reis; 29.11.2011
comment
Я знаю. Теперь я не знаю. Но я, возможно, захочу изменить это в какой-то момент в будущем. Отсюда изначальный вопрос. - person Fabian Zeindl; 29.11.2011

Я написал тестовый код, который показывает, что ReentrantLock в 10 раз медленнее, чем синхронизация в одном потоке.

Поскольку мой декодер пользовательского протокола TCP тратит 300 нс, скорость блокировки имеет значение.

ПДК 14:

  • ReentrantLock: 32,34 нс
  • синхронизировано: 3,48 нс

ПДК 11:

  • ReentrantLock: 30,3 нс
  • синхронизировано: 3,7 нс

Мой тест:

public class MyTest
{
    private ReentrantLock lock = new ReentrantLock();
    private String s;

    @Test
    public void test()
    {
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < 100_000_000; i++) {
            lock.lock();
            try {
                //
            } finally {
                lock.unlock();
            }
        }
        long t2 = System.currentTimeMillis();
        long d1 = t2 - t1;
        System.out.println("ReentrantLock: " + d1 / 100f + " ns");

        t1 = System.currentTimeMillis();
        for (int i = 0; i < 100_000_000; i++) {
            m1();
        }
        t2 = System.currentTimeMillis();
        long d2 = t2 - t1;
        t1 = System.currentTimeMillis();
        for (int i = 0; i < 100_000_000; i++) {
            m2();
        }
        t2 = System.currentTimeMillis();
        long d3 = t2 - t1;
        long d4 = d2 - d3;
        System.out.println("synchronized: " + d4 / 100f + " ns");
    }

    public synchronized void m1() {
        s = "1";
        s = "2";
        s = "3";
    }

    public void m2() {
        s = "1";
        s = "2";
        s = "3";
    }
}
person tibor17    schedule 26.07.2020
comment
Вы не можете сравнивать это таким образом, потому что фактические значения зависят, среди прочего, от количества потоков: lycog.com/concurency/performance-reentrantlock-synchronized Различные реализации JVM также могут выполнять оптимизацию, которую нелегко измерить. - person Fabian Zeindl; 30.07.2020
comment
JVM не может оптимизировать ReentrantLocks, потому что это только реализации Java. Вы также можете написать свои собственные объективные блокировки. Синхронизация — это не что иное, как monitorenter/monitorexit как две инструкции JVM, и их можно легко оптимизировать. - person tibor17; 31.07.2020
comment
Здесь, в моем примере, я говорил об одном потоке. Вопрос в том, почему этот парень использует блокировки в одном потоке. Ответ заключается в том, что моя реализация полиморфна. И в одной ситуации приложение работает с одним потоком. В другой конфигурации пользователь может задать множество потоков. - person tibor17; 31.07.2020
comment
30 наносекунд на самом деле ничего. Худшие времена наступают, когда поток T1 получает уведомление от другого потока T2, и T1 должен проснуться. Там речь идет о микросекундах. И это худшее время. - person tibor17; 31.07.2020