У меня есть веб-приложение, в котором я нахожусь в процессе тестирования нагрузки / производительности, особенно в отношении функции, где мы ожидаем, что несколько сотен пользователей будут получать доступ к одной и той же странице и будут обновлять ее каждые 10 секунд. Одна из областей улучшения, которую мы обнаружили с помощью этой функции, заключалась в кэшировании ответов от веб-службы в течение некоторого периода времени, поскольку данные не меняются.
После реализации этого базового кеширования в ходе дальнейшего тестирования я обнаружил, что не думаю, как параллельные потоки могут одновременно получать доступ к кешу. Я обнаружил, что в течение ~ 100 мс около 50 потоков пытались извлечь объект из кэша, обнаружили, что срок его действия истек, нажали веб-службу для извлечения данных и затем поместили объект обратно в кеш.
Исходный код выглядел примерно так:
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
final String key = "Data-" + email;
SomeData[] data = (SomeData[]) StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
}
else {
logger.debug("getSomeDataForEmail: using cached object");
}
return data;
}
Итак, чтобы убедиться, что только один поток вызывал веб-сервис, когда истек срок действия объекта key
, я подумал, что мне нужно синхронизировать операцию получения / установки кэша, и казалось, что использование ключа кэша будет хороший кандидат на объект для синхронизации (таким образом, вызовы этого метода для электронной почты b@b.com не будут блокироваться вызовами методов на a@a.com).
Я обновил метод, чтобы он выглядел так:
private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
SomeData[] data = null;
final String key = "Data-" + email;
synchronized(key) {
data =(SomeData[]) StaticCache.get(key);
if (data == null) {
data = service.getSomeDataForEmail(email);
StaticCache.set(key, data, CACHE_TIME);
}
else {
logger.debug("getSomeDataForEmail: using cached object");
}
}
return data;
}
Я также добавил строки регистрации для таких вещей, как «до блока синхронизации», «внутри блока синхронизации», «собирается покинуть блок синхронизации» и «после блока синхронизации», чтобы я мог определить, эффективно ли я синхронизировал get / установить операцию.
Однако, похоже, это не сработало. Мои тестовые журналы имеют вывод вроде:
(вывод журнала - «имя потока», «имя регистратора», «сообщение»)
http-80-Processor253 jsp.view-page - getSomeDataForEmail: собирается войти в блок синхронизации
http-80-Processor253 jsp.view-page - getSomeDataForEmail: внутри блока синхронизации
http-80-Processor253 cache.StaticCache - get: объект с ключом [SomeData-test@test.com] истек
http-80-Processor253 cache.StaticCache - получить: ключ [SomeData-test@test.com] возвращаемое значение [ноль]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: собирается войти в блок синхронизации
http-80-Processor263 jsp.view-page - getSomeDataForEmail: внутри блока синхронизации
http-80-Processor263 cache.StaticCache - get: объект с ключом [SomeData-test@test.com] истек
http-80-Processor263 cache.StaticCache - получить: ключ [SomeData-test@test.com] возвращаемое значение [ноль]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: собирается войти в блок синхронизации
http-80-Processor131 jsp.view-page - getSomeDataForEmail: внутри блока синхронизации
http-80-Processor131 cache.StaticCache - get: объект с ключом [SomeData-test@test.com] истек
http-80-Processor131 cache.StaticCache - получить: ключ [SomeData-test@test.com] возвращаемое значение [ноль]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: внутри блока синхронизации
http-80-Processor104 cache.StaticCache - get: объект с ключом [SomeData-test@test.com] истек
http-80-Processor104 cache.StaticCache - получить: ключ [SomeData-test@test.com] возвращаемое значение [ноль]
http-80-Processor252 jsp.view-page - getSomeDataForEmail: собирается войти в блок синхронизации
http-80-Processor283 jsp.view-page - getSomeDataForEmail: собирается войти в блок синхронизации
http-80-Processor2 jsp.view-page - getSomeDataForEmail: собирается войти в блок синхронизации
http-80-Processor2 jsp.view-page - getSomeDataForEmail: внутри блока синхронизации
Я хотел видеть только один поток за раз, входящий / выходящий из блока синхронизации вокруг операций get / set.
Есть ли проблема с синхронизацией на объектах String? Я думал, что ключ кеша будет хорошим выбором, так как он уникален для операции, и хотя в методе объявлено final String key
, я думал, что каждый поток получит ссылку на одного и того же объекта. и, следовательно, будет синхронизация на этом единственном объекте.
Что я здесь не так делаю?
Обновление : после просмотра журналов, кажется, что методы с той же логикой синхронизации, где ключ всегда одинаков, например
final String key = "blah";
...
synchronized(key) { ...
не показывают ту же проблему параллелизма - только один поток за раз входит в блок.
Обновление 2 : Спасибо всем за помощь! Я принял первый ответ о intern()
ing Strings, который решил мою первоначальную проблему - когда несколько потоков входили в синхронизированные блоки там, где я думал, что не должны, потому что key
имели одинаковое значение.
Как уже отмечали другие, использование intern()
для такой цели и синхронизация с этими строками действительно оказываются плохой идеей - когда я запускал тесты JMeter для веб-приложения для имитации ожидаемой нагрузки, я видел использованную кучу размер увеличивается почти до 1 Гб всего за 20 минут.
В настоящее время я использую простое решение - просто синхронизировать весь метод - но я действительно похож на примеры кода, предоставляемые martinprobst и MBCook, но так как у меня есть около 7 подобных getData()
методов в этом В настоящее время класс (так как ему требуется около 7 различных частей данных из веб-службы), я не хотел добавлять почти дублирующую логику получения и освобождения блокировок для каждого метода. Но это определенно очень, очень ценная информация для будущего использования. Я думаю, что это, в конечном счете, правильные ответы о том, как лучше сделать такую операцию безопасной для потока, и я бы отдал больше голосов за эти ответы, если бы мог!