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


183

java.util.concurrentAPI предоставляет класс с именем as Lock, который в основном сериализует элемент управления для доступа к критическому ресурсу. Это дает такой метод, как park()и unpark().

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

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


1
полезная статья здесь: javarevisited.blogspot.in/2013/03/…
roottraveller

Ответы:


178

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

Пример:

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

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

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

synchronized(myObject) {
    doSomethingNifty();
}

Тем не менее, Locks может быть более полезным для более сложных вещей, которые вы не можете получить и выпустить таким чистым способом. Честно говоря, я бы предпочел не использовать голые Locks в первую очередь и просто использовать более сложный контроль параллелизма, такой как a CyclicBarrierили aLinkedBlockingQueue , если они удовлетворяют вашим потребностям.

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


1
В чем разница между ожиданием / уведомлением и парковкой / разгрузкой LockSupport? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
Pacerier

6
Сначала пример имел смысл с блокировками, но потом я понял, что если вы используете блоки try finally, то этой проблемы можно избежать, если блокировки не снимаются
Уильям Рид

Ааа ... Один из тех моментов, чтобы оценить модель RAII в C ++. std::lock_guard
ВИЗМ

67

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

Я обнаружил, что 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)).

...


23

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

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

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

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


2
Является ли старое старое ожидание / уведомление примитивом более низкого уровня, чем java.util.concurrent.locks.LockSupport's park / unpark, или это наоборот?
Pacerier

@Pacerier: Я считаю, что оба они низкоуровневые (то есть то, что программист приложения хотел бы избегать использовать напрямую), но, конечно, низкоуровневые части java.util.concurrency (такие как пакет блокировок) построены поверх родных JVM-примитивов wait / notify (которые даже более низкого уровня).
Тило

2
Нет, я имею в виду из 3: Thread.sleep / interrupt, Object.wait / notify, LockSupport.park / unpark, который является самым примитивным?
Pacerier

2
@Thilo Я не уверен, как вы поддерживаете ваше утверждение, java.util.concurrentкоторое легче [в целом], чем языковые особенности ( synchronizedи т. Д.). Когда вы используете, java.util.concurrentвы должны привыкнуть к завершению lock.lock(); try { ... } finally { lock.unlock() }перед написанием кода, тогда как с a у synchronizedвас все в порядке с самого начала. На этом основании я бы сказал, synchronizedчто проще (если вы хотите, чтобы его поведение), чем java.util.concurrent.locks.Lock. пар 4
Иван Аукамп

1
Не думайте, что вы можете точно воспроизвести поведение классов AtomicXXX только с примитивами параллелизма, поскольку они полагаются на собственный вызов CAS, недоступный до java.util.concurrent.
Дункан Армстронг

15

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

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

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

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

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

Мне понравились детали вашего комментария. Я хотел бы добавить еще один пункт - ReadWriteLock обеспечивает полезное поведение, если вы имеете дело с несколькими потоками, только некоторые из которых должны записывать объект. Несколько потоков могут одновременно считывать объект и блокироваться, только если другой поток уже пишет в него.
Сэм Голдберг

5

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

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

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

Преимущества блокировки по синхронизации со страницы документации

  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замков.


4

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

synchronized(foo) {
}

или

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

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

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

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


5
«Синхронизация на уровне метода обеспечивает справедливое или FIFO-распределение блокировки». => Действительно? Вы говорите, что синхронизированный метод ведет себя иначе, чем честность, чем перенос содержимого методов в синхронизированный блок {}? Я бы так не думал, или я неправильно понял это предложение ...?
weiresr

Да, хотя это удивительно и противоречит интуиции, это правильно. Книга Гетца - лучшее объяснение.
Брайан Тарбокс

Если вы посмотрите на код, предоставленный @BrianTarbox, синхронизированный блок использует некоторый объект, отличный от «this», для блокировки. Теоретически нет никакой разницы между синхронизированным методом и помещением всего тела указанного метода в синхронизированный блок, если блок использует «this» в качестве блокировки.
xburgos

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

Извините, я только что обнаружил, что несколько дней назад ошибочно проголосовал против этого ответа (неуклюжий щелчок). К сожалению, ТАК в настоящее время не позволяет мне вернуть его. Надеюсь, я смогу это исправить позже.
Микко Остлунд,

3

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


2

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

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

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


1

Основное различие между блокировкой и синхронизацией:

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

0

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

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

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


} // end of randomFunction

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.