Статьи

Объектная микроблокировка для параллельных приложений с использованием Guava

Одной из наиболее раздражающих проблем при написании параллельных Java-приложений является обработка ресурсов, которые совместно используются потоками, например, сеанс веб-приложений и данные приложений. В результате многие разработчики предпочитают вообще не синхронизировать такие ресурсы, если уровень параллелизма приложения низкий. Например, маловероятно, что к ресурсу сеанса обращаются одновременно: если циклы запроса завершаются в течение короткого промежутка времени, маловероятно, что пользователь когда-либо отправит параллельный запрос, используя вторую вкладку браузера, пока первый цикл запроса еще выполняется. С появлением Ajax-управляемых веб-приложений этот доверительный подход становится все более опасным. В Ajax-приложении пользователь может, например, запросить выполнение более длительной задачи при запуске аналогичной задачи в другом окне браузера. Если эти задачи обращаются или записывают данные сеанса, вам необходимо синхронизировать такой доступ. В противном случае вы столкнетесь с незначительными ошибками или даже проблемами безопасности, как это, например, указано в этой записи блога .

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

01
02
03
04
05
06
07
08
09
10
11
HttpSession session = request.getSession(true);
if (session.getAttribute("shoppingCart") == null) {
  synchronize(session) {
    if(session.getAttribute("shoppingCart")= null) {
      cart = new ShoppingCart();
      session.setAttribute("shoppingCart");
    }
  }
}
ShoppingCart cart = (ShoppingCart)session.getAttribute("shoppingCart");
doSomethingWith(cart);

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

  1. Всякий раз, когда какое-либо значение добавляется к сеансу тем же способом, как описано выше, любой поток, который обращается к текущему сеансу, будет блокироваться. Это также произойдет, когда два потока попытаются получить доступ к различным значениям сеанса. Это блокирует приложение более ограниченным, чем это было бы необходимо.
  2. Реализация API сервлета может решить реализовать HttpSession, чтобы он не был единичным экземпляром. Если это так, вся синхронизация потерпит неудачу. (Это, однако, не распространенная реализация API сервлета.)

Было бы намного лучше найти другой объект для синхронизации экземпляра HttpSession. Создание таких объектов и разделение их между различными потоками, однако, привели бы к тем же проблемам. Хороший способ избежать этого — использовать кэши Guava, которые одновременно являются внутренними и допускают использование слабых ключей:

1
2
3
4
5
6
7
8
LoadingCache<String, Object> monitorCache = CacheBuilder.newBuilder()
       .weakValues()
       .build(
           new CacheLoader<String, Object>{
             public Object load(String key) {
               return new Object();
             }
           });

Теперь мы можем переписать код блокировки следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
HttpSession session = request.getSession(true);
Object monitor = ((LoadingCache<String,Object>)session.getAttribute("cache"))
  .get("shoppingCart");
if (session.getAttribute("shoppingCart") == null) {
  synchronize(monitor) {
    if(session.getAttribute("shoppingCart")= null) {
      cart = new ShoppingCart();
      session.setAttribute("shoppingCart");
    }
  }
}
ShoppingCart cart = (ShoppingCart)session.getAttribute("shoppingCart");
doSomethingWith(cart);

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

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