Эффективная синхронизация кеша - PullRequest
3 голосов
/ 25 октября 2010

Рассмотрим это

public Object doGet() {
    return getResource();
}

private Object getResource() {
    synchronized (lock) {
        if (cachedResourceIsStale()) {
            downloadNewVersionOfResource();
        }
    }

    return resource;
}

Если предположить, что doGet будет выполняться одновременно, и многое при этом, а загрузка новой версии ресурса занимает некоторое время, существуют ли более эффективные способы синхронизации в getResource? Я знаю о блокировках чтения / записи, но не думаю, что они могут быть применены здесь.

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

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

Ответы [ 2 ]

4 голосов
/ 25 октября 2010

Предположения

  1. эффективный означает, что doGet() должен завершиться как можно быстрее
  2. cachedPageIsStale() совсем не требует времени
  3. downloadNewVersionOfResource() занимает немного времени

Ответ

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

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

PS

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

Редактировать

Так? Предположим, у вас есть 100 рабочих соединений, и проверка того, является ли ресурс устаревшим, занимает одну микросекунду, ресурс не устарел, а его обслуживание занимает одну секунду. Затем в среднем 100 * 10 ^ -6 / 1 = 0,0001 потоки пытаются получить блокировку. Едва ли вообще разногласия. И накладные расходы на получение неиспользованной блокировки составляют порядка 10-8 секунд. Нет смысла оптимизировать вещи, которые уже принимают микросенды, когда сеть вызовет задержки в миллисекунды. И если вы мне не верите, сделайте микробенчмарк для синхронизации. Это правда, что частая ненужная синхронизация приводит к значительным накладным расходам, и что синхронизация классов сбора по этой причине устарела. Но это потому, что эти методы выполняют очень мало работы за вызов, а относительная нагрузка на синхронизацию была намного больше. Я только что сделал небольшой микробенчмарк для следующего кода:

synchronized (lock) {
    c++;
}

На моем ноутбуке это занимает 50 наносекунд (5 * 10 ^ -8 секунд) в среднем за 10 миллионов выполнений в солнечной точке доступа vm. Это примерно в 20 раз дольше, чем операция «голого» приращения, поэтому, если сделать много приращений, синхронизация каждого из них приведет к замедлению программы на порядок. Однако если бы этот метод блокировал ввод / вывод, ожидая, скажем, 1 мс, добавление тех же 50 наносекунд уменьшило бы пропускную способность на 0,005%. Наверняка у вас больше возможностей для настройки производительности: -)

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

1 голос
/ 25 октября 2010

Может существовать вероятность того, что вы можете уменьшить конфликт блокировок (таким образом, улучшив пропускную способность), используя «чередование блокировок» - по сути, разделите одну блокировку на несколько, каждая из которых защищает определенную группу пользователей.
Сложность в том, как понять, как распределять пользователей по группам.Самый простой случай - это когда вы можете назначить запрос от любого пользователя любой группе.Если ваша модель данных требует, чтобы запросы от одного пользователя обрабатывались последовательно, необходимо ввести некоторое сопоставление между запросами пользователей и группами.Вот пример реализации StripedLock:

import java.util.concurrent.locks.ReentrantLock;

/**
 * Striped locks holder, contains array of {@link java.util.concurrent.locks.ReentrantLock}, on which lock/unlock
 * operations are performed. Purpose of this is to decrease lock contention.
 * <p>When client requests lock, it gives an integer argument, from which target lock is derived as follows:
 * index of lock in array equals to <code>id & (locks.length - 1)</code>.
 * Since <code>locks.length</code> is the power of 2, <code>locks.length - 1</code> is string of '1' bits,
 * and this means that all lower bits of argument are taken into account.
 * <p>Number of locks it can hold is bounded: it can be from set {2, 4, 8, 16, 32, 64}.
  */
public class StripedLock {
    private final ReentrantLock[] locks;

    /**
     * Default ctor, creates 16 locks
     */
    public StripedLock() {
        this(4);
    }

    /**
     * Creates array of locks, size of array may be any from set {2, 4, 8, 16, 32, 64} 
     * @param storagePower size of array will be equal to <code>Math.pow(2, storagePower)</code>
     */
    public StripedLock(int storagePower) {
        if (storagePower < 1 || storagePower > 6)
             throw new IllegalArgumentException("storage power must be in [1..6]");
        int lockSize = (int) Math.pow(2, storagePower);
        locks = new ReentrantLock[lockSize];
        for (int i = 0; i < locks.length; i++)
            locks[i] = new ReentrantLock();
    }

    /**
     * Locks lock associated with given id.
     * @param id value, from which lock is derived
     */
    public void lock(int id) {
        getLock(id).lock();
    }

    /**
     * Unlocks lock associated with given id.
     * @param id value, from which lock is derived 
     */
    public void unlock(int id) {
        getLock(id).unlock();
    }

    /**
     * Map function between integer and lock from locks array
     * @param id argument
     * @return lock which is result of function 
     */
    private ReentrantLock getLock(int id) {
        return locks[id & (locks.length - 1)];
    }
}
...