Вложенные синхронизированные блоки в интернированных строках - PullRequest
4 голосов
/ 05 декабря 2009

Название звучит так, как будто впереди много проблем. Вот мой конкретный случай:

Это система продажи проездных билетов. Каждый маршрут имеет ограниченное количество билетов, поэтому покупка последнего билета для данного маршрута не должна быть доступна для двух человек (стандартный сценарий). Однако есть опция «обратный билет». Итак, я использую уникальный идентификатор маршрута (предоставленный базой данных) для следующих действий:

synchronized(bothRoutesUniqueString.intern()) {
    synchronized (routeId.intern()) {
        if (returnRouteId != null) {
            synchronized (returnRouteId.intern()) {
                return doPurchase(selectedRoute, selectedReturnRoute);
            }
        }
        return doPurchase(selectedRoute, selectedReturnRoute);
    }
}

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

Самый внешний блок synchronized предназначен для сценария, когда два человека покупают одну и ту же комбинацию билетов в обратном порядке. Например, один заказывает Лондон-Манчестер, а другой заказывает Манчестер-Лондон. Если нет внешнего синхронизированного блока, эта ситуация может привести к тупику.

(метод doPurchase() либо возвращает объект Ticket, либо выдает исключение, если больше нет доступных билетов)

Теперь я прекрасно понимаю, что это очень неловкое решение, но, если оно работает так, как ожидается, оно дает:

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

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

Я думаю, что горизонтальное масштабирование может быть достигнуто с помощью приведенного выше кода с использованием кластеризации VM. Согласно документации Teracotta , она позволяет превратить одноузловое многопоточное приложение в многоузловое и:

Terracotta отслеживает вызовы String.intern () и гарантирует равенство ссылок для этих явно интернированных строк. Поскольку все ссылки на интернированный объект String указывают на каноническое значение, проверки на равенство ссылок будут работать, как ожидается, даже для распределенных приложений.

Итак, теперь перейдем к самим вопросам:

  • замечаете ли вы какие-либо недостатки вышеуказанного кода (кроме его неловкости)?
  • существует ли подходящий класс из java.util.concurrent API, чтобы помочь в этом сценарии?
  • почему блокировка базы данных предпочтительнее для этого?

Обновление: Поскольку большинство ответов касаются OutOfMemoryError, я поставил оценку для intern(), и память не была израсходована. Возможно, таблица строк очищается, но в моем случае это не имеет значения, поскольку мне нужно, чтобы объекты были равны в условиях гонки, и очистка самых последних строк не должна происходить в точке:

System.out.println(Runtime.getRuntime().freeMemory());
for (int i = 0; i < 10000000; i ++) {
    String.valueOf(i).intern();
}
System.out.println(Runtime.getRuntime().freeMemory());

P.S. Среда JRE 1.6

Ответы [ 6 ]

8 голосов
/ 05 декабря 2009

почему блокировка базы данных предпочтительнее для этого?

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

2 голосов
/ 06 декабря 2009

Я могу предложить два совета.

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

2) Вы задаете этот вопрос явно и только в присутствии Терракоты? Если это так, то нет необходимости проходить стажировки. Возможно, это неочевидно, но при преобразовании синхронизированного (String) в терракотовый замок Terracotta блокирует VALUE строки, а не идентификатор. Очевидно, что если вы полагаетесь на это поведение, то вам следует прокомментировать его, поскольку интернирование, как вы это делали, требуется в любом случае, кроме присутствия Terracotta, поэтому любой стандартный Java-программист, взглянув на ваш код, будет по праву испуган:)

2 голосов
/ 05 декабря 2009

Interning - это умеренно дорогая операция, и есть небольшая вероятность, что она потребляет больше процессорного времени, чем возможные альтернативы. Но, конечно же, я не вижу, чтобы это занимало больше времени, чем запрос к базе данных. Единственный сценарий, в котором я мог бы представить себе выигрыш в реализации на основе БД, это если у вас так много потоков, выполняющих это параллельно, что вы были бы рады позволить БД выполнить часть работы, поэтому ваш ЦП будет ждать, но не работать тем временем.

Для по общему признанию ограниченного объема вашего предполагаемого применения ваше решение выглядит блестящим для меня.

2 голосов
/ 05 декабря 2009

Я бы сказал, что основным недостатком является interning объектов синхронизации.

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

С другой стороны, если в некоторых реализациях intern карта не ограничена по размеру, существует вероятность того, что вам может не хватить памяти.

Я бы не полагался на внутреннюю логику intern и создал бы свой собственный объект для хранения укусов и замков.

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

1 голос
/ 11 декабря 2009

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

От общей вероятности столкновений зависит, какая схема будет лучше - оптимистическая или пессимистическая.

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

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

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

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

Прочтите эту страницу (особенно раздел 11.3) для более подробной информации о поддержке параллелизма и блокировки Hibernate.

1 голос
/ 05 декабря 2009

Лучше всего использовать одиночный RouteProvider, который дает маршрут только один раз, блокируя или возвращая ноль, если маршрут уже используется. Я имею в виду общую конструкцию типа GenericObjectPool с одним дублем на маршрут.

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

Вам потребуется взять () и освободить () маршрут, чтобы RouteProvider знал, что он снова доступен. Не забудьте учесть исключения и release () в предложении finally:)

Во всяком случае, я бы никогда не пошел за чем-то, зависящим от реализации, в качестве интернированной строки.

...