0% нашли этот документ полезным (0 голосов)
15 просмотров6 страниц

Java Concurrency

Information

Загружено:

rtyuturn
Авторское право
© All Rights Reserved
Мы серьезно относимся к защите прав на контент. Если вы подозреваете, что это ваш контент, заявите об этом здесь.
Доступные форматы
Скачать в формате PDF, TXT или читать онлайн в Scribd
0% нашли этот документ полезным (0 голосов)
15 просмотров6 страниц

Java Concurrency

Information

Загружено:

rtyuturn
Авторское право
© All Rights Reserved
Мы серьезно относимся к защите прав на контент. Если вы подозреваете, что это ваш контент, заявите об этом здесь.
Доступные форматы
Скачать в формате PDF, TXT или читать онлайн в Scribd

2 Потокобезопасность

Возможно, вас удивит, что конкурентное программирование связано с по-


токами или замкˆами1 не более, чем гражданское строительство связано
с заклепками и двутавровыми балками. Разумеется, строительство мостов
требует правильного использования большого количества заклепок и дву-
тавровых балок, и то же самое касается построения конкурентных программ,
которое требует правильного использования потоков и замкˆов. Но это всего
лишь механизмы — средства достижения цели. Написание потокобезо-
пасного кода — это, по сути, управление доступом к состоянию и, в част-
ности, к совместному (shared) мутируемому состоянию (mutable state).

В целом состояние объекта — это его данные, хранящиеся в переменных


состояния (state variables), таких как экземплярные и статические поля
или поля из других зависимых объектов. Состояние хеш-массива HashMap
частично хранится в самом объекте HashMap, но также и во многих объектах
[Link]. Состояние объекта включает любые данные, которые могут
повлиять на его поведение.

1
В тексте встречаются термины lock и block, которые часто переводятся одним
словом «блокировка», что может подразумевать и объект, и процесс. В англий-
ском языке для процесса блокирования как приостановки продвижения есть
термин blocking. Под термином lock имеется в виду «замок», «замковый защит-
ный механизм». Во избежание путаницы термин lock переводится, как замок,
кроме устоявшихся выражений, где принят перевод «блокировка». Замок — это
механизм контроля доступа к данным с целью их защиты. В программировании
замки часто используются для того, чтобы несколько программ или программ-
ных потоков могли использовать ресурс совместно, например, обращаться
к файлу для его обновления на поочередной основе. — Примеч. науч. ред.
Потокобезопасность    49

К совместной переменной могут обратиться несколько потоков, мутиру‑


емая — меняет свое значение. На самом деле мы пытаемся защитить от
неконтролируемого конкурентного доступа не код, а данные.

Создание потокобезопасного объекта требует синхронизации для коорди-


нации доступа к мутируемому состоянию, невыполнение которой может
привести к повреждению данных и другим нежелательным последствиям.

Всякий раз, когда более чем один поток обращается к переменной состо‑
яния и один из потоков, возможно, в нее пишет, все потоки должны коор‑
динировать свой доступ к ней с помощью синхронизации. Синхронизацию
в Java обеспечивают ключевое слово synchronized, дающее эксклюзивную
блокировку, а также волатильные (volatile) и атомарные переменные и
явные замкˆи.

Удержитесь от соблазна думать, что существуют ситуации, не требующие


синхронизации. Программа может работать и проходить свои тесты, но
оставаться неисправной и завершиться аварийно в любой момент.

Если многочисленные потоки обращаются к одной и той же переменной,


имеющей мутируемое состояние, без соответствующей синхронизации,
то ваша программа неисправна. Существует три способа ее исправить:

•• не использовать переменную состояния совместно во всех потоках;

•• сделать переменную состояния немутируемой;

•• при каждом доступе к переменной состояния использовать синхронизацию.

Исправления могут потребовать значительных проектных изменений,


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

Будут или нет многочисленные потоки обращаться к той или иной пере-
менной, узнать сложно. К счастью, объектно‑ориентированные техни-
ческие решения, которые помогают создавать хорошо организованные
и удобные в сопровождении классы — такие как инкапсуляция и со-
крытие данных, — также помогают создавать потокобезопасные классы.
Чем меньше потоков имеет доступ к определенной переменной, тем про-
ще обеспечить синхронизацию и задать условия, при которых к данной
переменной можно обращаться. Язык Java не заставляет вас инкапсули-
ровать состояние — вполне допустимо хранить состояние в публичных
50   Глава 2. Потокобезопасность

полях (даже публичных статических полях) или публиковать ссылку на


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

При проектировании потокобезопасных классов хорошие объектно‑ори-


ентированные технические решения: инкапсуляция, немутируемость
и четкая спецификация инвариантов — будут вашими помощниками.

Если хорошие объектно‑ориентированные проектные технические ре-


шения расходятся с потребностями разработчика, стоит поступиться
правилами хорошего проектирования ради производительности либо
обратной совместимости с устаревшим кодом. Иногда абстракция и ин-
капсуляция расходятся с производительностью — хотя и не так часто, как
считают многие разработчики, — но образцовая практика состоит в том,
чтобы сначала делать код правильным, а затем — быстрым. Старайтесь
задействовать оптимизацию только в том случае, если измерения произ-
водительности и потребности говорят о том, что вы обязаны это сделать1.

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


все потеряно. Вашу программу по‑прежнему можно сделать потокобезо-
пасной, но процесс будет сложнее и дороже, а результат — ненадежнее.
Глава 4 характеризует условия, при которых можно безопасно смягчать
инкапсуляцию переменных состояния.

До сих пор мы использовали термины «потокобезопасный класс» и «по-


токобезопасная программа» почти взаимозаменяемо. Строится ли потоко-
безопасная программа полностью из потокобезопасных классов? Не обя-
зательно: программа, которая состоит полностью из потокобезопасных
классов, может не быть потокобезопасной, и потокобезопасная программа
может содержать классы, которые не являются потокобезопасными.
Вопросы, связанные с компоновкой потокобезопасных классов, также

1
В конкурентном коде следует придерживаться этой практики даже больше,
чем обычно. Поскольку ошибки конкурентности чрезвычайно трудно воспро-
изводимы и не просты в отладке, преимущество небольшого прироста произ-
водительности на некоторых редко используемых ветвях кода может вполне
оказаться ничтожным по сравнению с риском, что программа завершится ава-
рийно в условиях эксплуатации.
2.1. Что такое потокобезопасность?    51

рассматриваются в главе 4. В любом случае понятие потокобезопасного


класса имеет смысл только в том случае, если класс инкапсулирует соб-
ственное состояние. Термин «потокобезопасность» может применяться
к коду, но он говорит о состоянии и может применяться только к тому
массиву кода, который инкапсулирует его состояние (это может быть
объект или вся программа целиком).

2.1. Что такое потокобезопасность?


Дать определение потокобезопасности непросто. Быстрый поиск в Google
выдает многочисленные варианты, подобные этим:

...может вызываться из многочисленных потоков программы без не-


желательных взаимодействий между потоками.

...может вызываться двумя или более потоками одновременно, не тре-


буя никаких других действий с вызывающей стороны.

Учитывая подобные определения, неудивительно, что мы находим по-


токобезопасность запутанной! Как отличить потокобезопасный класс от
небезопасного? Что мы вообще подразумеваем под словом «безопасный»?

В основе любого разумного определения потокобезопасности лежит по-


нятие правильности (correctness).

Правильность подразумевает соответствие класса своей спецификации.


Спецификация определяет инварианты (invariants), ограничивающие
состояние объекта, и постусловия (postconditions), описывающие эф-
фекты от операций. Как узнать, что спецификации для классов являются
правильными? Никак, но это не мешает нам их использовать после того,
как мы убедили себя, что код работает. Поэтому давайте допустим, что
однопоточная правильность — это нечто видимое. Теперь можно пред-
положить, что потокобезопасный класс ведет себя правильно во время
доступа из многочисленных потоков.

Класс является потокобезопасным, если он ведет себя правильно во


время доступа из многочисленных потоков, независимо от того, как
выполнение этих потоков планируется или перемежается рабочей сре-
дой, и без дополнительной синхронизации или другой координации со
стороны вызывающего кода.
52   Глава 2. Потокобезопасность

Многопоточная программа не может быть потокобезопасной, если она не


является правильной даже в однопоточной среде1. Если объект реализо-
ван правильно, то никакая последовательность операций — обращения
к публичным методам и чтение или запись в публичные поля — не должна
нарушать его инварианты или постусловия. Ни один набор операций, вы‑
полняемых последовательно либо конкурентно на экземплярах потокобезо‑
пасного класса, не может побудить экземпляр находиться в недопустимом
состоянии.

Потокобезопасные классы инкапсулируют любую необходимую синхро-


низацию сами и не нуждаются в помощи клиента.

2.1.1. Пример: сервлет без поддержки внутреннего


состояния
В главе 1 мы перечислили структуры, которые создают потоки и вызывают
из них компоненты, за потокобезопасность которых ответственны вы. Те-
перь мы намерены разработать сервлетную службу разложения на множите-
ли и постепенно расширить ее функционал, сохраняя потокобезопасность.

В листинге 2.1 показан простой сервлет, который распаковывает число из за-


проса, раскладывает его на множители и упаковывает результаты в отклик.

Листинг 2.1. Сервлет без поддержки внутреннего состояния


@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}

Класс StatelessFactorizer, как и большинство сервлетов, не имеет внутрен-


него состояния: не содержит полей и не ссылается на поля из других классов.
Состояние для конкретного вычисления существует только в локальных

1
Если нестрогое использование термина правильность здесь вас беспокоит, то
вы можете думать о потокобезопасном классе как о классе, который неисправен
в конкурентной среде, как и в однопоточной среде.
2.2. Атомарность    53

переменных, которые хранятся в потоковом стеке и доступны только для


выполняющего потока. Один поток, обращающийся к Stateless­Factorizer,
не может повлиять на результат другого потока, делающего то же самое,
поскольку эти потоки не используют состояние совместно.

Объекты без поддержки внутреннего состояния всегда являются потоко­


безопасными.

Тот факт, что большинство сервлетов могут быть реализованы без под-
держки внутреннего состояния, значительно снижает бремя по обеспече-
нию потокобезопасности самих сервлетов. И только когда сервлеты долж-
ны что-то запомнить, требования к их потокобезопасности возрастают.

2.2. Атомарность
Что происходит при добавлении элемента состояния в объект без поддерж-
ки внутреннего состояния? Предположим, мы хотим добавить счетчик
посещений, который измеряет число обработанных запросов. Можно до-
бавить в сервлет поле с типом long и приращивать его при каждом запросе,
как показано в UnsafeCountingFactorizer в листинге 2.2.

Листинг 2.2. Сервлет, подсчитывающий запросы без необходимой


синхронизации. Так делать не следует

@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;

public long getCount() { return count; }

public void service(ServletRequest req, ServletResponse resp) {


BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}

Вам также может понравиться