У меня есть webapp, который я нахожусь в середине тестирования нагрузки и производительности, особенно на функции, где мы ожидаем, что несколько сотен пользователей будут получать доступ к одной и той же странице и будут обновляться каждые 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
истек, мне показалось, что мне нужно синхронизировать операцию get/set Cache, и казалось, что использование ключа кеша будет хороший кандидат на объект для синхронизации (таким образом, вызов этого метода для электронной почты [email protected] не будет заблокирован вызовами методов на адрес [email protected]).
Я обновил метод, чтобы он выглядел следующим образом:
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: объект в ключе [[email protected]] истек
http-80-Processor253 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor263 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor263 cache.StaticCache - get: объект с ключом [[email protected]] истек
http-80-Processor263 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor131 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor131 cache.StaticCache - get: объект в ключе [[email protected]] истек
http-80-Processor131 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor104 cache.StaticCache - get: объект с ключом [[email protected]] истек
http-80-Processor104 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
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) { ...
не отображаются те же проблемы concurrency - только один поток за один раз входит в блок.
Обновление 2: Спасибо всем за помощь! Я принял первый ответ о intern()
ing Strings, который решил мою первоначальную проблему - когда несколько потоков входили в синхронизированные блоки, где я думал, что они не должны, потому что значение key
имеет одинаковое значение.
Как указывали другие, использование intern()
для такой цели и синхронизация на этих строках действительно оказывается плохой идеей - при запуске тестов JMeter против webapp для имитации ожидаемой нагрузки я видел использованную кучу размер увеличивается почти до 1 ГБ менее чем за 20 минут.
В настоящее время я использую простое решение просто синхронизировать весь метод - но я действительно, например, образцы кода, предоставленные martinprobst и MBCook, но так как у меня около 7 подобных методов getData()
в этот класс в настоящее время (поскольку ему требуется около 7 различных частей данных из веб-службы), я не хотел добавлять почти дублирующую логику о получении и освобождении блокировок для каждого метода. Но это определенно очень, очень ценная информация для будущего использования. Я думаю, что это, в конечном счете, правильные ответы на то, как лучше всего сделать такую операцию, как эта потокобезопасная, и я бы дал больше голосов на эти ответы, если бы мог!