Замените ReadWriteLock на AtomicReference для неблокирующих операций

Я написал этот класс для перезагрузки DataSource, используемого всем приложением, при изменении сохраненных данных конфигурации. Как видите, он управляется CDI и отображается как Singleton, а событие «конфигурация изменена» поступает через метод configurationReload(...), но сейчас это не актуально.

Эталонное обновление охраняется ReentrantReadWriteLock, но мне интересно, нужно ли оно вообще.

@Singleton
@ThreadSafe
class ReloadingDataSource implements DataSource {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();

    @GuardedBy("readWriteLock")
    private DataSource delegateDataSource;

    @Inject
    ReloadingDataSource(@Nonnull final Configuration configuration) {
        delegateDataSource = createDataSource(configuration);
    }

    private DataSource createDataSource(final Configuration configuration) {
        ... Create a ComboPooledDataSource using properties extracted from Configuration.
    }

    @Override
    public Connection getConnection() throws SQLException {
        readLock.lock();

        try {
            return delegateDataSource.getConnection();
        } finally {
            readLock.unlock();
        }
    }

    ...

    private void configurationReload(
            @Observes @Reload final ConfigurationChanged configurationChanged,
            @Nonnull final Configuration configuration) {
        final ConfigurationEvent event = configurationChanged.getConfigurationEvent();

        if (event.getType() != AbstractFileConfiguration.EVENT_RELOAD && !event.isBeforeUpdate()) {
            return;
        }

        writeLock.lock();

        try {
            destroyDelegateDataSource();
            delegateDataSource = createDataSource(configuration);
        } finally {
            writeLock.unlock();
        }
    }

    private void destroyDelegateDataSource() {
        try {
            DataSources.destroy(delegateDataSource);
        } catch (final SQLException ignored) {
            // Do nothing.
        }
    }
}

Если мы проигнорируем стоимость создания нового источника данных, может ли вышеуказанная стратегия быть заменена на AtomicReference<DataSource>, как показано ниже?
Это привело бы к повышению производительности и облегчению чтения кода.

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

@Singleton
@ThreadSafe
class ReloadingDataSource implements DataSource {
    private final AtomicReference<DataSource> delegateDataSource;

    @Inject
    ReloadingDataSource(@Nonnull final Configuration configuration) {
        delegateDataSource = new AtomicReference<>(createDataSource(configuration));
    }

    private DataSource createDataSource(final Configuration configuration) {
        ... Create a ComboPooledDataSource using properties extracted from Configuration.
    }

    @Override
    public Connection getConnection() throws SQLException {
        return delegateDataSource.get().getConnection();
    }

    ...

    private void configurationReload(
            @Observes @Reload final ConfigurationChanged configurationChanged,
            @Nonnull final Configuration configuration) {
        final ConfigurationEvent event = configurationChanged.getConfigurationEvent();

        if (event.getType() != AbstractFileConfiguration.EVENT_RELOAD && !event.isBeforeUpdate()) {
            return;
        }

        // Updated as per eckes tip. Is this what you meant?
        final DataSource newDataSource = createDataSource(configuration);

        while (true) {
            final DataSource oldDataSource = delegateDataSource.get();

            if (delegateDataSource.compareAndSet(oldDataSource, newDataSource)) {
                destroyDelegateDataSource(oldDataSource);
                break;
            }
        }
    }

    private void destroyDelegateDataSource(final DataSource oldDataSource) {
        try {
            DataSources.destroy(oldDataSource);
        } catch (final SQLException ignored) {
            // Do nothing.
        }
    }
}

Мне кажется, что если вы можете сгенерировать новый экземпляр, атомарное обновление - это нормально. Однако во всех случаях необходимо учитывать, когда следует уничтожить старый экземпляр. Тот факт, что getConnection не выполняется, не означает, что объекты из этого источника данных все еще используются. В зависимости от того, нужно ли вам ссылаться на старое состояние или нет, вы можете вывести createDataStore () из цикла или даже пропустить compareAndSet.

eckes 08.10.2018 20:05

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

jtahlborn 08.10.2018 20:09

@eckes Ты прав! Я упустил этот момент. Я должен тебе кофе.

LppEdd 08.10.2018 20:09

@jtahlborn Да, во время создания «первого» нового источника данных может наблюдаться несколько изменений. Но я не понимаю, какую проблему это может создать? Я имею в виду, что «второй» и последующие потоки будут просто ждать получения блокировки.

LppEdd 08.10.2018 20:12

@eckes Я обновил пример Atomic, как вы предложили. Не могли бы вы проверить, правильно ли я понял, что вы имели в виду? Пропустить compareAndSet, потому что эталонное обновление само по себе является атомарным? Это мне не понятно

LppEdd 08.10.2018 20:17

обновления конфигурации "заказаны"? если вы начнете обрабатывать update1, а затем появится update2 (с «более новой» информацией), вы можете оказаться в ситуациях, когда окончательный результат основан на потерях изменений update1 и update2.

jtahlborn 08.10.2018 20:30

@jtahlborn Да, обновления заказываются. При обработке update1 я нахожусь в области блокировки записи, поэтому, если появляется update2, он ждет в writeLock.lock (), пока update1 не выполнит writeLock.unlock (). Таким образом, update2 просто переопределяет update1, поскольку он следует тому же пути кода. Но может я ошибаюсь

LppEdd 08.10.2018 20:34

Я про вашу новую версию, в которой нет блокировки записи?

jtahlborn 08.10.2018 20:56

@jtahlborn Ага, извини, у меня плохо! Что вы предлагаете в таком случае?

LppEdd 08.10.2018 21:01

Да, новая версия похожа на то, что я предлагал. Я думаю, вы можете обойтись без цикла и вместо этого использовать getAndSet(). Он также вернет старое значение, необходимое для его уничтожения (в конечном итоге). Но я подозреваю, особенно если ваши события изменения однопоточные, оба достаточно быстрые.

eckes 08.10.2018 21:19

@eckes, второй вариант кажется мне очень неработающим, похоже, это какая-то блокировка спина. Предположим, что этот if (delegateDataSource.compareAndSet(oldDataSource, newDataSource)) { возвращает true - на данный момент никто не останавливает delegateDataSource.get(), чтобы продолжить, и, как я понимаю, это должны быть отдельные операции

Eugene 08.10.2018 21:21

@Eugene Я буду ждать ответа eckes, потому что я заблудился и не хочу писать чушь.

LppEdd 08.10.2018 21:30

@eckes getAndSet кажется прекрасным, в основном он завершает то, что я делаю сейчас

LppEdd 08.10.2018 21:41

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

eckes 09.10.2018 04:38

Я также предлагаю вместо этого использовать некоторый источник данных для пула, так как это может лучше управлять состоянием открытых или заимствованных соединений. Однако не уверен, какие из них поддерживают чистую реконфигурацию.

eckes 09.10.2018 04:42

@eckes Я использую объединенную реализацию от C3P0. Читая javadoc для уничтожения, он фактически не заставляет ресурсы закрыться. В нем также указано, что ресурсы очищаются при финализации, поэтому я могу просто разыменовать старый источник данных. Может быть...

LppEdd 09.10.2018 07:36

@LppEdd Если переконфигурирование происходит редко, все в порядке.

eckes 09.10.2018 11:56
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
17
319
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если вам требуется, чтобы ваши обновления обрабатывались упорядоченным образом, вам все равно потребуется заблокировать метод перезагрузки. В этом случае вы можете отказаться от логики AtomicReference и просто использовать volatile:

public class RDS {
  private volatile DataSource delegate;

  public Connection getConnection() throws SQLException {
    return delegate.getConnection();
  }

  private void reload(Configuration config) {
    DataSource old = null;
    synchronized(this) {
      old = delegate;
      delegate = createDataSource(config);
    }
    destroyDataSource(old);
  }
}

Обратите внимание, однако, что у вас все еще потенциально есть другие проблемы, когда соединения могут все еще использоваться для старого источника данных, когда вы его закрываете (упоминается в первом комментарии @eckes к вопросу). Чтобы исправить это, вам понадобится что-то вроде пула соединений с логикой типа получения / выпуска, который закрывает старый делегат после того, как все существующие соединения будут освобождены.

Другие вопросы по теме