Синхронизация против блокировки

java.util.concurrent API предоставляет класс под названием Lock, который в основном сериализовал бы управление, чтобы получить доступ к критическому ресурсу. Это дает такой метод, как park() а также unpark(),

Мы можем делать подобные вещи, если мы можем использовать synchronized ключевое слово и использование wait() а также notify() notifyAll() методы.

Мне интересно, какой из них лучше на практике и почему?

12 ответов

Решение

Если вы просто блокируете объект, я бы предпочел использовать synchronized

Пример:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Вы должны явно сделать try{} finally{} везде.

Принимая во внимание, что с синхронизированным, это супер ясно и невозможно ошибиться:

synchronized(myObject) {
    doSomethingNifty();
}

Это сказало, LockОни могут быть более полезны для более сложных вещей, которые вы не можете получить и выпустить таким чистым способом. Я бы честно предпочел не использовать голые Locks в первую очередь, и просто пойти с более сложным управлением параллелизма, таким как CyclicBarrier или LinkedBlockingQueue, если они отвечают вашим потребностям.

У меня никогда не было причин использовать wait() или же notify() но могут быть и хорошие.

Мне интересно, какой из них лучше на практике и почему?

Я нашел это Lock а также Condition (и другие новые concurrent классы) просто больше инструментов для панели инструментов. Я мог сделать почти все, что мне было нужно с моим старым молотком с раздвоенным хвостом (synchronized ключевое слово), но это было неудобно для использования в некоторых ситуациях. Некоторые из этих неловких ситуаций стали намного проще, когда я добавил в свой ящик для инструментов больше инструментов: резиновый молоток, молоток с шариковой ручкой, монтировку и несколько ударов по гвоздям. Тем не менее, мой старый молоток с раздвоенным хвостом все еще видит свою долю использования.

Я не думаю, что один действительно "лучше", чем другой, но каждый лучше подходит для разных задач. Короче говоря, простая модель и сфера synchronized помогает защитить меня от ошибок в моем коде, но те же самые преимущества иногда являются помехами в более сложных сценариях. Это более сложные сценарии, для решения которых был создан параллельный пакет. Но использование конструкций более высокого уровня требует более четкого и тщательного управления в коде.

===

Я думаю, что JavaDoc хорошо описывает разницу между Lock а также synchronized (акцент мой):

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

...

Использование синхронизированных методов или операторов обеспечивает доступ к неявной блокировке монитора, связанной с каждым объектом, но заставляет все получение и снятие блокировки происходить блочно-структурированным способом: при получении нескольких блокировок они должны быть сняты в обратном порядке, и все замки должны быть освобождены в той же лексической области, в которой они были приобретены.

Хотя механизм определения объема для синхронизированных методов и операторов значительно облегчает программирование с помощью блокировок монитора и помогает избежать многих распространенных ошибок программирования, связанных с блокировками, бывают случаи, когда вам нужно работать с блокировками более гибким способом. Например, * * некоторые алгоритмы * для обхода одновременно доступных структур данных требуют использования "передачи с передачей" или "цепной блокировки": вы получаете блокировку узла A, затем узла B, затем освобождаете A и получаете C, затем отпустите B и получите D и так далее. Реализации интерфейса Lock позволяют использовать такие методы, позволяя получать и снимать блокировку в разных областях, и позволяя получать и снимать несколько блокировок в любом порядке.

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

...

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

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

...

Вы можете достичь всего, что утилиты в java.util.concurrent делают с низкоуровневыми примитивами, такими как synchronized, volatileили подождите / сообщите

Тем не менее, параллелизм сложен, и большинство людей понимают, по крайней мере, некоторые его части неправильно, что делает их код либо неправильным, либо неэффективным (или оба).

Параллельный API предоставляет высокоуровневый подход, который проще (и, как таковое, безопаснее) использовать. В двух словах, вам не нужно использовать synchronized, volatile, wait, notify прямо больше.

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

Есть 4 основных фактора, почему вы хотели бы использовать synchronized или же java.util.concurrent.Lock,

Примечание: синхронная блокировка - это то, что я имею в виду, когда говорю внутреннюю блокировку.

  1. Когда в Java 5 появились ReentrantLocks, оказалось, что они имеют довольно заметную разницу в пропускной способности, чем встроенная блокировка. Если вы ищете более быстрый механизм блокировки и используете 1.5, рассмотрите jucReentrantLock. Собственная блокировка Java 6 теперь сопоставима.

  2. У jucLock есть разные механизмы блокировки. Блокировка прерывается - попытка блокировки до тех пор, пока блокирующая нить не будет прервана; временная блокировка - попытка заблокировать на определенное время и сдаться, если у вас ничего не получится; tryLock - попытка блокировки, если какой-то другой поток удерживает блокировку, сдаться. Это все включено помимо простого замка. Внутренняя блокировка предлагает только простую блокировку

  3. Стиль. Если и 1, и 2 не попадают в категории того, что вас волнует, большинство людей, включая меня, считают, что внутренняя блокировка семенатики проще для чтения и менее многословна, чем блокировка jucLock.
  4. Несколько условий. Объект, на который вы блокируете, может быть уведомлен и ожидать только одного случая. Метод newCondition Локка позволяет одному замку иметь множество причин для ожидания или подачи сигнала. На практике мне пока не нужна эта функциональность, но это хорошая функция для тех, кто в ней нуждается.

Я хотел бы добавить еще кое-что поверх ответа Берта Ф.

Locks поддержка различных методов для более точного управления блокировками, которые более выразительны, чем неявные мониторы (synchronized замки)

Блокировка обеспечивает эксклюзивный доступ к общему ресурсу: только один поток за раз может получить блокировку, и весь доступ к общему ресурсу требует, чтобы блокировка была получена первой. Однако некоторые блокировки могут разрешать одновременный доступ к общему ресурсу, например, блокировка чтения ReadWriteLock.

Преимущества Lock over Synchronization со страницы документации

  1. Использование синхронизированных методов или операторов обеспечивает доступ к неявной блокировке монитора, связанной с каждым объектом, но заставляет все получение и освобождение блокировки происходить блочно-структурированным способом.

  2. Реализации блокировки предоставляют дополнительные функциональные возможности по сравнению с использованием синхронизированных методов и операторов, предоставляя неблокирующую попытку получения lock (tryLock())попытка получить блокировку, которая может быть прервана (lockInterruptibly()и попытка получить замок, который может timeout (tryLock(long, TimeUnit)),

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

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

ReentrantLock ключевые особенности согласно этой статье

  1. Возможность блокировки непрерывно.
  2. Возможность тайм-аута в ожидании блокировки.
  3. Сила, чтобы создать справедливый замок.
  4. API для получения списка ожидающих потоков для блокировки.
  5. Гибкость, чтобы попытаться заблокировать без блокировки.

Ты можешь использовать ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock для дальнейшего контроля над гранулярной блокировкой операций чтения и записи.

Помимо этих трех ReentrantLocks, Java 8 предоставляет еще один замок

StampedLock:

Java 8 поставляется с новым типом блокировки под названием StampedLock, который также поддерживает блокировки чтения и записи, как в примере выше. В отличие от ReadWriteLock, методы блокировки StampedLock возвращают штамп, представленный длинным значением.

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

Посмотрите на эту статью об использовании различных типов ReentrantLock а также StampedLock замки.

Основным отличием является справедливость, другими словами, обрабатываются ли запросы FIFO или может быть завал? Синхронизация на уровне метода обеспечивает справедливое или FIFO-распределение блокировки. С помощью

synchronized(foo) {
}

или же

lock.acquire(); .....lock.release();

не гарантирует справедливости.

Если у вас много споров за блокировку, вы можете легко столкнуться с блокировкой, когда новые запросы блокируют, а более старые запросы застревают. Я видел случаи, когда 200 потоков поступали в короткие сроки для блокировки, а 2-й поток, который пришел, обрабатывался последним. Это нормально для некоторых приложений, но для других это смертельно.

См. Книгу Брайана Гетца "Параллелизм Java на практике", раздел 13.3, для полного обсуждения этой темы.

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

Блокировка облегчает жизнь программистам. Вот несколько ситуаций, которые могут быть достигнуты с помощью блокировки.

  1. Блокировка в одном методе и снятие блокировки в другом методе.
  2. У вас есть два потока, работающие над двумя разными частями кода, однако в первом потоке существует зависимость от второго потока, который завершает определенный фрагмент кода, прежде чем продолжить работу (в то время как некоторые другие потоки также работают одновременно). Совместная блокировка может решить эту проблему довольно легко.
  3. Реализация мониторов. Например, простая очередь, в которой методы put и get выполняются из множества разных потоков. Однако вы не хотите перекрывать ни те же методы один на другой, ни методы put и get. В таком случае закрытый замок очень облегчает жизнь.

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

Книга Брайана Гетца "Параллелизм Java на практике", раздел 13.3: "... Как и стандартный ReentrantLock, встроенная блокировка не дает никаких детерминированных гарантий справедливости, но гарантии статистической справедливости большинства реализаций блокировки достаточно хороши практически для всех ситуаций..."

Вот полное сравнение сLock

Синхронизированные преимущества:

  • Автоматический выпуск: нет необходимости в попытке-наконец, защита от забытого выпуска.

             synchronized {
         someCode();
     }
    

    против

             lock.lock();
     try {
         someCode();
     } finally {
         lock.release();
     }
    
  • Меньше лишних строк (пример выше)

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

             public synchronized someMethod() {
         someCode();
     }
    

Преимущества замка:

  • Расширенный API:.lockInterruptibly(),.tryLock(),tryLock(timeout)
  • Возможность создания несколькихConditionсвязано с замком. Например,ArrayBlockingQueueдержит обаnotEmptyиnotFullусловия.
  • Возможность получить поток, удерживающий блокировку, во время отладки черезsync.exclusiveOwnerThread. API для получения состояния блокировки:.getHoldCount(),.hasQueuedThreads()и т. д.
  • Возможность передачи как объект. Например, держитеMap<ResourceObject, Lock>
  • Возможность захвата и снятия нескольких блокировок в любом порядке.
  • Умение создавать ярмарки:new ReentrantLock(true)
  • СуществованиеReadWriteLockразрешение параллельного чтения

Блокировка и синхронизация блока служат одной и той же цели, но это зависит от использования. Рассмотрим следующую часть

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

В приведенном выше случае, если поток входит в блок синхронизации, другой блок также блокируется. Если на одном объекте несколько таких блоков синхронизации, все блоки заблокированы. В таких ситуациях java.util.concurrent.Lock может использоваться для предотвращения нежелательной блокировки блоков.

Многие ответы здесь рекомендуют использовать synchronized.

Однако это зависит от варианта использования.

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

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

Таким образом, для меньшей системы, которая может обходиться без параллелизма и позволяющая одному потоку выполнять операцию, synchronized может работать. В противном случае блокировку можно будет взять на ключ.

Другие вопросы по тегам