Страницы

Поиск по вопросам

пятница, 17 мая 2019 г.

Немного вопросов о многопоточности

Сам не первый год пишу на java, но лишь в рамках хобби, с многопоточностью приходится не так часто работать.
Заинтересовали несколько вопросов, а имённо:
Допустим, мне нужно писать и вычитывать примитивный тип на каком-то объекте (из разных потоков). Допустим, поля открытые (public).
Если верить справке java, операции чтения и записи ссылок являются атомарными (логично, ведь размер такой ссылки обычно помещается в регистр процессора, да и система его поддерживает). Тогда чтение и запись поля, хранящего ссылку на объект, вполне себе нормальная операция для выполнения в разных потоках.
Также справка java уверяет, что чтение и запись примитивных типов, исключая long и double, тоже являются атомарными. Но как быть с этими двумя? Как я понимаю, их нужно лишь пометить как volatile. Но есть ли какие-то побочные эффекты, или этот модификатор только и делает, что гарантирует атомарность операций над переменной?
Хорошо. А теперь, к примеру, я хочу читать и писать поля через геттеры-сеттеры. Если мои геттеры-сеттеры не меняют состояние каких-либо внешних данных, а лишь читают и пишут переменную, достаточно ли мне пометить такую переменную как volatile, или же нужно отмечать геттер/сеттер как syncronized? (При условии, что доступ к переменным возможен лишь через геттер и сеттер). Работа с объектами. А конкретно, с объектами, что не меняют состояния.
К примеру, String. Насколько мне известно, данный тип никогда не меняет состояния, а все его методы, возвращающие строку, возвращают новый экземпляр.
Так вот, могу ли я обращаться к такому объекту из разных потоков без дополнительной синхронизации? Или же я могу тогда что-то упустить (вопрос для случая, когда я могу лишь получить объект, но не изменить значение поля, на него ссылающегося).
Благодарю за терпение того, кто сможет на эти вопросы ответить)


Ответ

Важно, что volatile гарантирует видимость. Изменения volatile-поля в одном потоке будут сразу видны в другом. Если вам нужно потокобезопасно выполнить более сложную процедуру, чем присвоение - воспользуйтесь synchronized или j.u.c.Atomic*-типами, чтобы избежать возникновения гонок (race condition).
Пример: любой многопоточный счетчик
// Плохо private volatile long counter;
public long getId() { counter += 1; return counter; }
// Хорошо private volatile AtomicLong counter = new AtomicLong(0);
public long getId() { return counter.incrementAndGet(); }
// Тоже хорошо private volatile long counter;
public synchronized long getId() { counter += 1; return counter; }
Помните, что synchronized - это всегда явная блокировка (но она довольно дёшева при низкой конкуренции), а Atomic-примитивы используют бесконечный цикл с CAS-инструкциями и могут хорошо оптимизироваться.

Объекты с неизменяемым состоянием (например, все поля такого объекта final) назвают immutable объектами, и с ними безопасно работать из разных потоков. Но если внутреннее состояние объекта содержит изменяемый какой-то компонент и он как-то может быть доступен снаружи - придется дополнительно думать о потокобезопасности. Пример:
// потокобезопасный класс public final class SafeFoo { private final Date date = new Date(); }
// не потокобезопасный класс public final class UnsafeFoo { private final Date date = new Date(); public Date getDate() { return date; } }
Первый класс полностью потокобезопасен. Второй - нет. Метод getDate() осуществляет публикацию внутреннего состояния внешнему коду (unsafe publication). И где-то снаружи можно сделать так, изменив внутренне состояние:
unsafeFoo.getDate().setHours(0);
Защититься от этого можно, например созданием копии:
// потокобезопасный класс public final class SafeAgainFoo { private final Date date = new Date(); public Date getDate() { return date.clone(); } }

Комментариев нет:

Отправить комментарий