Вопрос-ответ

Avoid synchronized(this) in Java?

Избегать synchronized (this) в Java?

Всякий раз, когда на SO появляется вопрос о синхронизации Java, некоторые люди очень хотят указать, что synchronized(this) этого следует избегать. Вместо этого, как они утверждают, предпочтительнее блокировка частной ссылки.

Вот некоторые из приведенных причин:

Другие люди, включая меня, утверждают, что synchronized(this) это идиома, которая часто используется (в том числе в библиотеках Java), безопасна и хорошо понятна. Этого не следует избегать, потому что у вас ошибка и вы понятия не имеете, что происходит в вашей многопоточной программе. Другими словами: если это применимо, то используйте его.

Мне интересно увидеть несколько реальных примеров (без foobar), где предпочтительнее избегать блокировкиthis, когда synchronized(this) это также выполнит работу.

Следовательно: следует ли вам всегда избегать synchronized(this) и заменять это блокировкой частной ссылки?


Дополнительная информация (обновляется по мере получения ответов):


  • мы говорим о синхронизации экземпляров

  • рассматриваются как неявные (synchronized методы), так и явные формы synchronized(this)

  • если вы цитируете Блоха или другие авторитетные источники по этому вопросу, не упускайте те части, которые вам не нравятся (например, Effective Java, пункт о потокобезопасности: обычно это блокировка самого экземпляра, но бывают исключения.)

  • если вам нужна детализация в вашей блокировке, отличная от synchronized(this) обеспечиваемой, то synchronized(this) это неприменимо, так что проблема не в этом

Переведено автоматически
Ответ 1

Я рассмотрю каждый пункт отдельно.



  1. Какой-то злой код может украсть вашу блокировку (очень популярный этот, также есть вариант "случайно")



    Меня больше беспокоит случайность. Это означает, что такое использование this является частью открытого интерфейса вашего класса и должно быть задокументировано. Иногда желательно, чтобы другой код мог использовать вашу блокировку. Это верно для таких вещей, как Collections.synchronizedMap (см. javadoc ).



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



    Это слишком упрощенное мышление; простое избавление от synchronized(this) не решит проблему. Правильная синхронизация пропускной способности потребует больше размышлений.



  3. Вы (без необходимости) предоставляете слишком много информации



    Это вариант # 1. Использование synchronized(this) является частью вашего интерфейса. Если вы не хотите / нужно, чтобы это было открыто, не делайте этого.


Ответ 2

Ну, во-первых, следует отметить, что:

public void blah() {
synchronized (this) {
// do stuff
}
}

семантически эквивалентно:

public synchronized void blah() {
// do stuff
}

что является одной из причин не использовать synchronized(this). Вы можете возразить, что вы можете делать что-то вокруг synchronized(this) блока. Обычная причина заключается в том, чтобы попытаться вообще избежать выполнения синхронизированной проверки, что приводит ко всевозможным проблемам параллелизма, в частности к проблеме двойной блокировки с двойной проверкой, которая просто показывает, насколько сложно может быть сделать относительно простую проверку потокобезопасной.

Закрытая блокировка - это защитный механизм, который никогда не является плохой идеей.

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

synchronized(this) просто на самом деле ничего вам не дает.

Ответ 3

При использовании synchronized (this) вы используете экземпляр класса как саму блокировку. Это означает, что пока поток 1 получает блокировку, поток 2 должен ждать.

Предположим, что следующий код:

public void method1() {
// do something ...
synchronized(this) {
a ++;
}
// ................
}


public void method2() {
// do something ...
synchronized(this) {
b ++;
}
// ................
}

Методом 1 модифицируя переменную a и методом 2 модифицируя переменную b, следует избегать одновременного изменения одной и той же переменной двумя потоками, и это так. НО пока thread1 модифицирует a и thread2 модифицирует b, это может выполняться без каких-либо условий гонки.

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

Решение заключается в использовании 2 разных блокировок для двух разных переменных:

public class Test {

private Object lockA = new Object();
private Object lockB = new Object();

public void method1() {
// do something ...
synchronized(lockA) {
a ++;
}
// ................
}


public void method2() {
// do something ...
synchronized(lockB) {
b ++;
}
// ................
}

}

В приведенном выше примере используются более детализированные блокировки (2 блокировки вместо одной (lockA и lockB для переменных a и b соответственно) и, как результат, обеспечивается лучший параллелизм, с другой стороны, он стал более сложным, чем в первом примере ...

Ответ 4

Хотя я согласен с тем, что нельзя слепо придерживаться догматических правил, сценарий "кражи блокировок" кажется вам таким эксцентричным? Поток действительно может получить блокировку вашего объекта "извне" (synchronized(theObject) {...}), блокируя другие потоки, ожидающие синхронизированных методов экземпляра.

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

"Случайная" версия кажется менее вероятной, но, как говорится, "сделайте что-нибудь идиотское, и кто-нибудь придумает идиота получше".

Итак, я согласен со школой мышления "это зависит от того, что делает класс".


Отредактируйте первые 3 комментария eljenso:

Я никогда не сталкивался с проблемой кражи блокировок, но вот воображаемый сценарий:

Допустим, ваша система представляет собой контейнер сервлетов, а объект, который мы рассматриваем, является ServletContext реализацией. Его getAttribute метод должен быть потокобезопасным, поскольку атрибуты контекста являются общими данными; поэтому вы объявляете его как synchronized. Давайте также представим, что вы предоставляете общедоступную услугу хостинга на основе вашей контейнерной реализации.

Я ваш клиент и развертываю свой "хороший" сервлет на вашем сайте. Случается, что мой код содержит вызов getAttribute.

Хакер, замаскированный под другого клиента, развертывает свой вредоносный сервлет на вашем сайте. Он содержит следующий код в init методе:

синхронизировано (this.getServletConfig().getServletContext()) {
while (true) {}
}

Предполагая, что мы используем один и тот же контекст сервлета (разрешено спецификацией, пока два сервлета находятся на одном виртуальном хосте), мой вызов на getAttribute заблокирован навсегда. Хакер добился DoS на моем сервлете.

Эта атака невозможна, если getAttribute синхронизирована с частной блокировкой, потому что сторонний код не может получить эту блокировку.

Я признаю, что этот пример является надуманным и представляет собой чрезмерно упрощенное представление о том, как работает контейнер сервлетов, но ИМХО это доказывает суть.

Итак, я бы сделал свой выбор дизайна, исходя из соображений безопасности: буду ли я иметь полный контроль над кодом, который имеет доступ к экземплярам? Каковы будут последствия того, что поток будет удерживать блокировку экземпляра бесконечно?

2023-11-27 15:24 java multithreading