Должны ли геттеры/сеттеры примитивных типов блокироваться с помощью ReadWriteLock в многопоточном приложении?

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

class Example {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private int i;
    private boolean b;

    public int getI() {
      lock.readLock().lock();
      final int tmp = i;
      lock.readLock().unlock(),
      return tmp;
    }

    public void setI( final int i ) {
      lock.writeLock().lock();
      this.i = i;
      lock.writeLock().unlock();
    }

    public boolean getB() {
      lock.readLock().lock();
      final boolean tmp = b;
      lock.readLock().unlock(),
      return tmp;
    }

    public void setB( final boolean b ) {
      lock.writeLock().lock();
      this.b = b;
      lock.writeLock().unlock();
    }
}

Для простоты в этом примере я исключил блоки try...finally вокруг замков.

Мне интересно, нужно ли (или, скажем, рекомендуется) блокировать/синхронизировать геттеры и сеттеры примитивных типов? Я знаю, что операции присваивания и возврата в Java атомарны. Однако, используя эти блокировки, разве я не удостоверяюсь, что каждый метод доступа получает самое последнее значение (равно использованию volatile)?

Что, если бы примитивы были double или long?


person Sven Jacobs    schedule 16.02.2011    source источник


Ответы (6)


Это зависит.

Обратите внимание, однако, что обычно вам нужно синхронизировать операции на более крупном уровне, например, это:

Example e = ...;

synchronized (e) {
    e.setI(e.getI() + 10);
}

Для таких сценариев ваши внутренние блокировки являются излишними. Поэтому, возможно, было бы лучше применять внешнюю синхронизацию там, где вы используете эти объекты, а не внутреннюю синхронизацию.

person axtavt    schedule 16.02.2011

у вас есть что-то вроде AtomicInteger в java, которое хорошо работает с многопоточным приложением.

person Dead Programmer    schedule 16.02.2011

Спросите себя, можно ли этот класс реализовать как неизменяемый класс. С ним будет проще работать, и он будет по своей сути потокобезопасным. Вам не придется беспокоиться о параллелизме, блокировках, синхронизации и т. д.

Пример неизменяемого класса:

final class Example {
    private final int i;
    private final boolean b;

    public Example(int i, boolean b){
        this.i = i ;
        this.b = b;
    }

    public int getI() {
        return i;
    }

    public boolean getB() {
        return b;
    }
}
person dogbane    schedule 16.02.2011
comment
К сожалению, неизменяемый класс здесь невозможен, так как некоторые свойства необходимо изменить во время выполнения. - person Sven Jacobs; 16.02.2011
comment
@Sven Возможно, вы сможете создать новый экземпляр класса с этими новыми свойствами вместо изменения свойств в существующем экземпляре. - person dogbane; 16.02.2011
comment
Свойства меняются очень часто. Не дороже ли в случае памяти и ЦП создать новый экземпляр, чем изменить свойство? - person Sven Jacobs; 16.02.2011
comment
@Sven Ваш объект выглядит довольно просто. Синхронизировать будет дороже. Сборщик мусора Java любит маленькие недолговечные объекты. Помните, что String, Integer и т. д. — все это неизменяемые классы. - person dogbane; 16.02.2011
comment
Пример класса был урезан для простоты. Фактический класс имеет 12 свойств, 6 из которых являются примитивами. - person Sven Jacobs; 16.02.2011

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

например Скажем, у вас есть 32-ядерная система, которая отлично масштабируется и работает в 32 раза быстрее, чем на 1 ядре. Однако доступ к полю без блокировки занимает 1 нс, а с блокировкой — 1 мс (1000 нс), поэтому в итоге ваше приложение может работать примерно в 30 раз медленнее. (на 1000 медленнее / на 32 быстрее) Если у вас всего 4 ядра, это может быть в сотни раз медленнее, что, в первую очередь, лишает смысла многопоточность. ПО МОЕМУ МНЕНИЮ.

person Peter Lawrey    schedule 16.02.2011

Нет необходимости блокировать/синхронизировать геттеры и сеттеры примитивных типов - в большинстве случаев достаточно пометить их как volatile (кроме двойных и длинных, как вы упомянули)

Как упоминалось в одном из предыдущих постов, вам нужно знать о последовательностях чтения и обновления, например, incrementI(int num), которые, скорее всего, вызовут getI() и setI() — в этом случае вы можете добавить «синхронизированный incrementI(int num)' в ваш класс Example. Затем блокировка выполняется на более высоком уровне, что снижает потребность в отдельных блокировках чтения и записи и является дружественным к ООП, поскольку данные и поведение остаются вместе. Этот метод еще более полезен, если вы читаете/обновляете несколько полей одновременно.

Хотя, если вы просто читаете/записываете/обновляете одно поле за раз, то классы AtomicXX более подходят.

person Dave Sturgeon    schedule 16.02.2011

Вы не должны использовать блокировку для примитивных типов, String (они неизменяемы) и потокобезопасных типов (например, коллекций из пакета "concurrent").

person yname    schedule 16.02.2011