Должны ли геттеры/сеттеры примитивных типов блокироваться с помощью ReadWriteLock в многопоточном приложении?
-
14-11-2019 - |
Вопрос
У меня есть класс 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
?
Решение
Это зависит.
Однако учтите, что обычно синхронизировать операции нужно на более грубом уровне, например так:
Example e = ...;
synchronized (e) {
e.setI(e.getI() + 10);
}
Для таких сценариев ваши внутренние блокировки избыточны.Поэтому, возможно, было бы лучше применить внешнюю синхронизацию там, где вы используете эти объекты, а не внутреннюю синхронизацию.
Другие советы
у вас есть что-то вроде AtomicInteger в Java, которое хорошо работает с многопоточным приложением.
Спросите себя, можно ли реализовать этот класс как неизменяемый класс.С ним будет проще работать, и он будет по своей сути потокобезопасным.Вам не придется беспокоиться о параллелизме, блокировках, синхронизации и т. д.
Пример неизменяемого класса:
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;
}
}
Я бы разработал ваше приложение так, чтобы у вас не было одновременного доступа к такому необработанному типу данных.Добавление такой низкоуровневой блокировки может настолько замедлить ваше приложение, что его не стоит использовать в многопоточном режиме.
напримерПредположим, у вас есть 32-ядерная система, которая прекрасно масштабируется и работает в 32 раза быстрее, чем на 1-ядерной системе.Однако доступ к полю без блокировки занимает 1 нс, с блокировкой — 1 мкс (1000 нс), поэтому в конечном итоге ваше приложение может работать примерно в 30 раз медленнее.(на 1000 медленнее / на 32 быстрее). Если у вас всего 4 ядра, оно может быть в сотни раз медленнее, что в первую очередь противоречит цели использования многопоточности.ИМХО.
Нет необходимости блокировать/синхронизировать геттеры и сеттеры примитивных типов - в большинстве случаев достаточно пометить их как изменчивые (кроме двойных и длинных, как вы упомянули)
Как упоминалось в одном из предыдущих сообщений, вам необходимо знать о последовательностях чтения и обновления, что-то вроде инкремента I(int num), который, скорее всего, вызовет getI() и setI() - в этом случае вы можете добавить «синхронизированный инкрементI(int num)' в ваш класс примера.Затем блокировка выполняется на более высоком уровне, что снижает необходимость в отдельных блокировках чтения и записи и является объектно-ориентированным, поскольку данные и поведение остаются вместе.Этот метод еще более полезен, если вы читаете/обновляете несколько полей одновременно.
Хотя если вы просто читаете/записываете/обновляете одно поле за раз, то классы AtomicXX более подходят.
Не следует использовать блокировку для примитивных типов, String (они неизменяемы) и потокобезопасных типов (например, коллекций из «параллельного» пакета).