JPL. Threads
по мотивам «Java Programming Language»
- Введение
- 1. Создание потоков
- 2. Использование
Runnable
- 3. Синхронизация
- 4. Методы
wait
,notifyAll
, иnotify
- 5. Ожидание (waiting) и уведомление (notification) в деталях
- 6. Диспетчеризация (scheduling) потоков
- 7. Взаимные блокировки (deadlock)
- 8. Завершение работы потоков
- 9. Завершение работы приложения
- 10. Модель памяти: синхронизация и
volatile
- 11. Управление потоками, безопасность, и
ThreadGroup
- 12. Потоки и исключения
- 13. Класс
ThreadLocal
- 14. Отладка потоков
Введение
Поток - это последовательность шагов, выполняемых по одному в каждый момент времени.
Потоки могут иметь общий доступ к одному и тому же объекту. Общий доступ одновременно является как одной из наиболее полезных возможностей многопоточности, так и её наибольшим недостатком.
Неопределённость параллелизма (или состояние гонки) существует когда два потока, потенциально, могут модифицировать (получение-изменение-присваивание) одну и ту же часть данных конкурентным образом, что может привести к её повреждению.
Для разрешения этой проблемы используются блокировки. Блокировка ассоциируется с объектом, обозначая находится ли объект в пользовании или нет.
Однопоточные системы обычно предоставляют иллюзию множественности потоков используя либо прерывания, либо механизм опрашивания. Например, код отображения должен периодически делать запросы на необходимость обновления экрана в ответ на пользовательский ввод.
1. Создание потоков
Создание потока выполнения начинается с создания объекта класса Thread
:
Thread worker = new Thread();
После этого его можно сконфигурировать задав начальный приоритет, имя и т.д. После, для запуска потока, вызывается метод Thread::start
.
Метод Thread::start
порождает новый поток управления на основе данных объекта Thread
и возвращает управление вызывающему коду. После этого JVM вызывает метод Thread::run
, делая поток активным. Thread::start
у объекта потока можно вызвать лишь единожды. Повторный вызов породит исключение IllegalThreadStateException
.
Поток завершается тогда, когда происходит возврат управления метода Thread::run
.
Стандартная реализация Thread::run
не делает ничего. Для того чтобы дать потоку задание нужно либо унаследоваться от класса Thread
и переопределить метод Thread::run
, либо создать объект класса Runnable
и передать его в конструктор класса Thread
.
public class PingPong extends Thread {
private String word; // what word to print
private int delay; // how long to pause
public PingPong(String whatToSay, int delayTime) {
word = whatToSay;
delay = delayTime;
}
@Override
public void run() {
try {
for (;;) {
System.out.print(word + " ");
Thread.sleep(delay); // wait until next time
}
} catch (InterruptedException e) {
return; // end this thread
}
}
public static void main(String[] args) {
new PingPong("ping", 33).start(); // 1/30 second
new PingPong("PONG", 100).start(); // 1/10 second
}
}
Получение объекта класса Thread
текущего потока выполняется с помощью вызова статического метода Thread::currentThread
.
Метод main
выполняется в потоке созданном системой времени выполнения.
2. Использование Runnable
Поток абстрагирует концепцию рабочей единицы (worker) - сущности, которая выполняет какую-то работу. Работа выполняемая потоком упаковывается в метод Thread::run
.
Класс Thread
также реализует интерфейс Runnable
.
Выполнение объекта Runnable
в отдельном потоке осуществляется через его передачу конструктору класса Thread
. Если объект класса Thread
создается с передачей объекта Runnable
, реализация Thread::run
вызовет метод Runnable::run
этого объекта.
Конструкторы класса Thread
:
-
Thread(target: Runnable)
- создает новый объекта классаThread
, который использует методrun
переданного объектаRunnable
. -
Thread(target: Runnable, name: String)
- создает новый объекта классаThread
с заданным именемname
, который использует методrun
переданного объектаRunnable
. -
Thread(group: ThreadGroup, target: Runnable)
- создает новый объекта классаThread
в указаннойThreadGroup
, который использует методrun
переданного объектаRunnable
. -
Thread(group: ThreadGroup, target: Runnable, name: String)
- создает новый объекта классаThread
в указаннойThreadGroup
, с заданным именем, который использует методrun
переданного объектаRunnable
. -
Thread(group: ThreadGroup, target: Runnable, name: String, stackSize: long)
- создает новый объекта классаThread
в указаннойThreadGroup
, с заданным именем, который использует методrun
переданного объектаRunnable
. ПараметрstackSize
позволяет передать желаемый размер стека для потока. Значение соответствующее размеру стека является системно-зависимым и система может проигнорировать переданное значение.
Пример более сложного класса:
class PrintServer implements Runnable {
private final PrintQueue requests = new PrintQueue();
public PrintServer() {
new Thread(this).start();
}
public void print(PrintJob job) {
requests.add(job);
}
public void run() {
for(;;)
realPrint(requests.remove());
}
private void realPrint(PrintJob job) {
// do the real work of printing
}
}
Запуск потока в конструкторе может быть рискованным в случае наследования, т.к. поток может получить доступ к полям объекта до того как выполнится конструктор наследника.
При создании объекта класса Thread
ссылка на него хранится в соответствующей ThreadGroup
, и таким образом предотвращается его удаление сборщиком (GC).
3. Синхронизация
Критическая секция или критическая область - участок кода в котором одновременное выполнение некоторого набора операций может привести к повреждению данных. Данная коллизия разрешается синхронизацией доступа к такой критической секции.
Потоки взаимодействуют удовлетворяя требованиям протокола, что перед тем как выполнить какую-либо операцию над объектом, необходимо получить его блокировку. Получение блокировки предотвращает её захват другими потоками до тех пор, пока текущий держатель не осовободит её. При корректной реализации множество потоков будут не способны одновременно выполнить действия, приводящие к интерференции.
Каждый объект имеет, ассоциированную с ним, блокировку. Эта блокировка может быть захвачена и освобождена с помощью синхронизации методов и выражений. Термин синхронизированный код обозначает любой код внутри синхронизированного метода или выражения.
При вызове потоком синхронизированного (synchronized
) метода объекта сначала происходит захват блокировки этого объекта, потом выполняется тело метода, и после этого блокировка освобождается. Другой поток, который вызвал этот метод у того же объекта, будет блокирован до освобождения блокировки.
Синхронизация обеспечивает взаимоисключающее во времени выполнение потоков.
Захват блокировок выполняется по-поточно, т.е. вызов метода синхронизированного на том же объекте, что и вызывающий, выполнится без блокировки. Освобождение блокировки выполнится на выходе из объемлющего метода.
Освобождение блокировки выполняется как при нормальном завершении (return
, конец тела метода), так и при аварийном (исключение).
Thread::holdsLock(obj: Object)
- проверка того, владеет ли текущий поток блокировкой данного объекта.
Синхронизация является частью реализации класса. В случае когда наследник переопределяет synchronized
-метод, новый метод может быть как синхронизированным, так и нет.
Статические методы также могут быть синхронизированы. В этом случае используется блокировка объекта Class
, ассоциированного с каждым классом.
Захват объекта класса Class
при вызове статического синхронизированного метода не оказывает влияния на объекты этого класса.
Выражение synchronized
дает возможность синхронизации произвольного блока кода с захватом блокировки любого объекта.
Синхронизированные методы являются всего лишь сокращенной записью метода, тело которого завернуто в блок synchronized
с ссылкой на this
.
Для таких объектов как массивы синхронизация на них самих является единственным средством контроля их разделяемого пользования.
Для защиты доступа к статическим данным из не статического кода можно использовать литерал класса этого объекта. Например:
Body() {
synchronized (Body.class) {
idNum = nextID++;
}
}
Клиентская синхронизация осуществляется путем получения всеми клиентами блокировки разделяемого объекта перед его использованием. Данный подход строится на том, что все клиенты придерживаются этого правила.
В случае синхронизации вызовов нескольких объектов есть вероятность создания deadlock’а.
class DeadLock {
synchronized void foo(DeadLock deadLock) {
System.out.println("DeadLock::foo() - "
+ Thread.currentThread().getName());
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
deadLock.bar();
}
synchronized void bar() {
System.out.println("DeadLock::bar() - "
+ Thread.currentThread().getName());
}
public static void main(String[] args) {
DeadLock dl1 = new DeadLock();
DeadLock dl2 = new DeadLock();
new Thread(() -> dl1.foo(dl2), "T1").start();
new Thread(() -> dl2.foo(dl1), "T2").start();
}
}
4. Методы wait
, notifyAll
, и notify
Коммуникация между потоками обеспечивается с помощью методов Object::wait
, Object::notify
и Object::notifyAll
. Метод Object::wait
дает потоку возможность ожидания наступления какого-либо состояния, а методы Object::notify
и Object::notifyAll
уведомляют ожидающие потоки о его наступлении. Данные методы применяются для конкретного объекта, также как и блокировки.
Существуют стандартные паттерны, которые следует применять при использовании wait
и notifу
. В потоке, ожидающем события следует делать что-то вроде:
synchronized void doWhenCondition() {
while (!condition) {
wait();
}
// … Do what must be done when the condition is true …
}
В этом примере важно отметить следующее:
-
метод является синхронизированным. Иначе состояние объекта было бы нестабильным, например не было бы гарантии в том, что условие будет оставаться истинным и после выражения
while
- другой поток мог бы его изменить. -
одним из важных аспектов
wait
является то, что при остановке потока, он атомарно освобождает блокировку объекта. Под атомарностью приостановки потока и освобождения блокировки понимается их выполнение как одной неделимой операции. Иначе есть вероятность попадания в состояние гонки. После получения уведомления и продолжения работы потока, повторная блокировка захватывается также атомарно. -
проверка условия всегда должна выполняться в цикле, т.е. не надо менять
while
наif
.
Код уведомления обычно выглядит так:
synchronized void changeCondition() {
// … change some value used in a condition test …
notifyAll(); // or notify()
}
Object::notifyAll
используется для уведомления всех ожидающих потоков, в то время как Object::notify
пробуждает только один поток.
Несколько потоков могут ожидать одного и того же объекта, но, возможно, в различных состояниях. В этом случае всегда следует использовать метод Object::notifyAll
. Object::notify
является оптимизацией, которая применима только в случае когда:
-
все потоки ожидают одного и того же состояния
-
хотя бы один поток извлечет пользу из данного состояния
-
все это обеспечено контрактом для всех возможных подклассов
Иначе всегда используйте Object::notifyAll
.
Использование Object::wait
означает приостановку потока до тех пор пока для него не появится работа.
5. Ожидание (waiting) и уведомление (notification) в деталях
Существует три формы ожидания и две формы уведомления:
-
wait(timeout: long): void throws InterruptedException
- текущий поток ожидает наступления одного из четырех событий: у объекта вызван методnotify
, и данный поток выбран для пробуждения; у объекта вызванnotifyAll
; истек указанный таймаут; или у потока был вызван методinterrupt
. Во время ожидания блокировка объекта освобождается, и, перед завершением, автоматически перезахватывается независимо от причины завершения.InterruptedException
возбуждается в случае завершения ожидания из-за прерывания (interrupt) потока. -
wait(timeout: long, nanos: int): void throws InterruptedException
- более точное указание таймаута (timeout
- миллисекунды,nanos
- наносекунды). -
wait(): void throws InterruptedException
- эквивалентенwait(0)
. В этом случае время ожидания неограниченно. -
notifyAll(): void
- уведомляет все потоки ожидающие изменения состояния. Потоки пробуждаются от ожидания по мере захвата блокировки. -
notify(): void
- уведомляет по меньшей мере один ожидающий поток. Выбор конкретного потока для уведомления невозможен.
Данные методы должны вызываться только внутри синхронизированного кода, используя блокировку того объекта для которого они вызываются. Вызов может делаться как в самом синхронизированном коде, так и в вызываемых методах. В случае вызова этих методов у объекта без захвата его блокировки будет возбуждено исключение IllegalMonitorStateException
.
Отслеживание ситуации когда поток пробудился по истечению таймаута необходимо реализовывать в вызывающем коде, т.к. встроенного механизма для этого нет.
Возможна (очень редко) ситуация когда поток будет пробужден от ожидания не будучи уведомленным, прерванным или по истечении таймаута. Для предотвращения такого ложного пробуждения при проверке условия продвижения потока следует всегда использовать цикл, а не условное выражение.
6. Диспетчеризация (scheduling) потоков
Для отражения важности выполняемой задачи потоки имеют приоритет (priority), который используется средой времени выполнения для определения того, какой поток будет запущен в конкретный момент времени. Планировщик потоков дает лишь общие гарантии.
Запущенный поток будет находиться в активном состоянии до выполнения блокирующей операции (такой как sleep
, wait
или некоторого типа I/O) или пока не будет прерван.
Приоритеты нужно использовать только для целей повышения эффективности, т.к. их использование не дает каких-либо гарантий относительно прерываний, захвата блокировок и порядка получения потоками уведомлений.
В целом рекомендуется задавать более низкий приоритет (Thread.NORM_PRIORITY - 1
) непрерывно выполняющимся потокам, чем потокам, которые обрабатывают такие события как, например, пользовательский ввод.
У класса Thread
есть несколько методов позволяющих потоку освободить используемые ресурсы ЦПУ. По умолчанию, статические методы класса Thread
всегда применяются к текущему потоку, и, так как невозможно получить ресурсы ЦПУ другого потока, эти методы объявлены как статические.
-
static sleep(millis: long): void throws InterruptedException
- усыпляет текущий поток на, по меньшей мере, указанное количество миллисекунд. При прерывании спящего потока будет возбуждено исключениеInterruptedException
. -
static sleep(millis: long, nanos: int): void throws InterruptedException
- усыпляет текущий поток на, по меньшей мере, указанное количество милли- и наносекунд. -
static yield(): void
- дает планировщику подсказку о том, что текущему потоку не требуется быть запущенным в настоящее время. Планировщик может как воспользоваться ей, так и проигнорировать.
Следующая программа иллюстрирует то как yield
влияет на планирование потоков:
class Babble extends Thread {
static boolean doYield; // yield to other threads?
static int howOften; // how many times to print
private String word; // my word
Babble(String whatToSay) {
word = whatToSay;
}
@Override
public void run() {
for (int i = 0; i < howOften; i++) {
System.out.println(word);
if (doYield) {
Thread.yield(); // let other threads run
}
}
}
public static void main(String[] args) {
doYield = new Boolean(args[0]).booleanValue();
howOften = Integer.parseInt(args[1]);
// create a thread for each word
for (int i = 2; i < args.length; i++) {
new Babble(args[i]).start();
}
}
}
В случае запуска с параметрами:
Babble false 2 Did DidNot
возможен следующий вывод:
Did
Did
DidNot
DidNot
В этом случае yield
не вызывается и каждый поток, обычно, получает достаточный отрезок времени чтобы завершить весь вывод без прерываний.
В случае запуска с параметрами:
Babble true 2 Did DidNot
возможен следующий вывод:
Did
DidNot
DidNot
Did
В этом случае вызов yield
дает другим потокам шанс быть запущенными.
7. Взаимные блокировки (deadlock)
Взаимную блокировку (deadlock) продемонстрируем на следующем классе:
class Friendly {
private Friendly partner;
private String name;
public Friendly(String name) {
this.name = name;
}
public synchronized void hug() {
System.out.println(Thread.currentThread().getName() +
" in " + name + ".hug() trying to invoke " +
partner.name + ".hugBack()");
partner.hugBack();
}
private synchronized void hugBack() {
System.out.println(Thread.currentThread().getName() +
" in " + name + ".hugBack()");
}
public void becomeFriend(Friendly partner) {
this.partner = partner;
}
}
Этот класс примечателен тем, что в синхронизированном методе hug
вызывается, также синхронизированный метод hugBack
другого объекта.
Теперь рассмотрим следующий код:
public static void main(String[] args) {
Friendly jareth = new Friendly("jareth");
Friendly cory = new Friendly("cory");
jareth.becomeFriend(cory);
cory.becomeFriend(jareth);
new Thread(jareth::hug, "Thread1").start();
new Thread(cory::hug, "Thread2").start();
}
Возможен следующий сценарий выполнения этого кода:
-
В потоке #1 вызывается синхронизированный метод
jareth::hug
- поток #1 владеет блокировкой объектаjareth
. -
В потоке #2 вызывается синхронизированый метод
cory::hug
- поток #2 владеет блокировкой объектаcory
. -
В методе
jareth::hug
вызывается методcory::hugBack
. Поток #1 теперь блокирован в ожидании блокировки объектаcory
, которой, в текущий момент, владеет поток #2. -
Наконец, в методе
cory::hug
вызывается синхронизированный методjareth::hugBack
и поток #2 блокируется в ожидании блокировки объектаjareth
, которым, в текущий момент, владеет поток #1.
Таким образом, произошла взаимная блокировка - cory::hug
не может продолжить выполнение до тех пор пока не получит блокировку объекта jareth
, и наоборот.
Одним из распространенных подходов для предотвращение взаимных блокировок является их упорядочивание (lock ordering), которое заключается в обеспечении фиксированного порядка захвата. В этом случае если первый поток захватит первую блокировку, то второй поток будет блокирован в ее ожидании, а первый поток сможет безопасно захватить следующую блокировку и т.д.
8. Завершение работы потоков
Запущенный поток будет оставаться живым (alive) до тех пор пока не завершится по одной из трех причин:
-
нормальный возврат из метода
Runnable::run
-
преждевременное завершение работы метода
Runnable::run
-
завершение приложения
Завершение потока выполнится как при нормальном возврате, так и при преждевременном завершении (например, при не перехваченном исключении). По завершению поток освобождает все захваченные блокировки.
В случае необходимости отменяемости (cancellable) работы потока нужно обеспечить отслеживание и соответствующий ответ на его прерывание (interruption).
Thread th1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// do a little work
}
});
th1.start();
th1.interrupt();
Прерывание (interruption) не обрывает работающий поток само по себе, хотя спящие и ожидающие потоки будут отменены. Данное поведение полезно тем, что работающий поток получает определенный контроль над обработкой собственной отмены.
К прерыванию потоков относятся следующие методы:
-
Thread::interrupt
- отправляет потоку отмену. -
Thread::isInterrupted
- возвращает признак того, что поток был отменен. -
Thread::interrupted
- статический метод, который возвращает признак отмены текущего потока, и после этого сбрасывает состояние отмены. Сброс состояния отмены потока возможен только им самим.
В случае отмены потока во время сна (Thread::sleap
) или ожидания (Thread::wait
) будет возбуждено исключение InterruptedException
. Обработка этого исключения обычно выглядит подобно следующему:
void tick(int count, long pauseTime) {
try {
for (int i = 0; i < count; i++) {
System.out.println('.');
System.out.flush();
Thread.sleep(pauseTime);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Т.к. возбуждение исключения отмены сбрасывает состояние прерванности, повторная отмена позволяет другому коду обработать данную ситуацию штатным образом.
В общем, все методы, выполняющие блокирующие операции (явно или неявно), должны допускать возможность быть прерванными, и возбуждать соответствующее исключение в этом случае.
Ожидание завершения потока выполняется с помощью одного из методов Thread::join
.
class CalcThread extends Thread {
private double result;
@Override
public void run() {
result = calculate();
}
public double getResult() {
return result;
}
public double calculate() {
// ... calculate a value for "result"
}
}
class ShowJoin {
public static void main(String[] args) {
CalcThread calc = new CalcThread();
calc.start();
doSomethingElse();
try {
calc.join();
System.out.println("result is "
+ calc.getResult());
} catch (InterruptedException e) {
System.out.println("No answer: interrupted");
}
}
// ... definition of doSomethingElse ...
}
В приведенном примере происходит запуск потока CalcThread
, выполняются какие-то действия и вызывается метод calc::join
. Возврат join
гарантирует завершение CalcThread.run
. Завершение потока не означает удаление его объекта, и join
может быть вызван на уже остановленном потоке.
Thread::join
имеет следующие три формы:
-
join(millis: long): void throws InterruptedException
- ожидает либо завершения данного потока, либо истечения указанного количества миллисекунд. Таймаут равный нулю означает вечное ожидание. При отмене ожидающего потока будет возбуждено исключениеInterruptedException
. -
join(millis: long, nanos: int): void throws InterruptedException
- ожидает либо завершения данного потока, либо истечения указанного количества милли- и наносекунд. -
join(): void throws InterruptedException
- эквивалентенjoin(0L)
.
Внутренне join
определен через isAlive
и логически может рассматриваться как:
while (isAlive()) {
wait();
}
с учетом того, что системой времени выполнения, при завершении потока, будет вызван метод notifyAll
.
9. Завершение работы приложения
Каждое приложение начинается с одного потока - того, который выполняет метод main
. Если больше потоков не создается, то приложение завершается при выходе из этого метода.
Существует два типа потоков: пользовательские и демоны. В отличие от демонов наличие работающего пользовательского потока не даст приложению завершиться. При завершении последнего пользовательского потока все демоны останавливаются и приложение завершается. Демоны останавливаются сразу, и таким образом, у них нет возможности выполнить какие-либо завершающие действия. Для выполнения потока как демона используется метод Thread::setDaemon
, а для проверки - Thread::isDaemon
. По-умолчанию, признак потока-демона наследуется от потока-создателя и не может быть изменен после запуска потока. В этом случае будет возбуждено исключение IllegalThreadStateException
.
Начальный поток является обычным пользовательским потоком.
Принудительная остановка приложения выполняется с помощью метода System::exit
. Данный метод приостанавливает выполнение JVM, и обрывает все потоки. Однако, можно зарегистрировать особые потоки, которые будут выполняться перед приостановкой.
Многие классы неявно создают потоки в приложении. Некоторые из этих потоков могут быть демонами, а некоторые - нет, и, таким образом, использование этих классов может стать причиной более длительной работы приложения при останове. Завершить такие потоки, в случае необходимости, можно с помощью метода System::exit
.
10. Модель памяти: синхронизация и volatile
Доступ к любым разделяемым и изменяемым данным должен выполняться синхронизированно. Однако, это приводит к определённым накладным расходам и не всегда необходимо для предотвращения вмешательства (interference). Платформа гарантирует, что операции записи и чтение переменных всех типов, кроме long
и double
будут выполняться атомарно, т.е. всегда содержать данные записанные каким-то одним потоком. Это означает, например, что атомарные переменные, значения которых пишутся одним потоком, а читаются - многими, не требуют синхронизации доступа.
Существует несколько факторов влияющих на то, когда значение переменной записанное одним потоком становится видимым другому. Правила, определяющие упорядочивание доступа к памяти и гарантии видимости её содержимого, известны как модель памяти (Java Memory Model).
Действия, выполняемые потоком, определяются семантикой выражений выполняемого метода. Логически эти выражения выполняются в порядке их записи, известном как программный порядок (program order). Однако, значения всех читаемых потоком переменных определяются моделью памяти. Модель памяти определяет набор допустимых значений переменной, которые могут быть возвращены при её чтении. Под переменными, в данном контексте, подразумеваются как статические, так и не статические поля и элементы массивов. С точки зрения программиста этот набор всегда должен состоять из единственного элемента - последнего, записанного каким-либо потоком, значения. Однако, при отсутствии соответствующей синхронизации, реальный набор возможных значений может содержать несколько элементов.
Рассмотрим пример:
public void updateCurrent() {
currentValue = (int) Math.random();
}
public void showValue() {
currentValue = 5;
for (;;) {
display.showValue(currentValue);
Thread.sleep(1000);
}
}
Предположим, что не синхронизированный метод updateCurrent
может вызываться произвольными потоками. При первом вхождении в цикл (в случае если updateCurrent
ещё не вызывался) единственно возможным значением currentValue
является 5
. Однако, из-за отсутствия синхронизации при каждом вызове другим потоком метода updateCurrent
в набор возможных возвращаемых значений добавляется ещё один элемент. В соответствии с правилами модели памяти при чтении может быть возвращено любое, записанное каким-либо потоком, значение. Таким образом, платформа может посчитать, что переменная currentValue
является неизменной и метод showValue
всегда будет выводить 5
.
Некоторые действия, выполняемые потоком, определяются как действия синхронизации, и последовательность, в которой они выполняются, называется порядком синхронизации (synchronization order) программы. Порядок синхронизации всегда соответствует программному порядку. Эти действия синхронизируют чтение и запись переменных.
Наиболее известным действием синхронизации является использование синхронизированных методов и блоков для обеспечения исключительного доступа к разделяемым переменным. Говорится, что освобождение блокировки монитора синхронизировано со всеми последующими захватами и освобождениями блокировки этого же монитора. Если все чтения и записи переменной выполняются только при захвате определенного монитора, тогда модель памяти гарантирует, что каждое чтение этой переменной вернет самое последнее записанное значение.
Существует еще один механизм синхронизации - volatile
-переменные. Запись volatile
-переменной синхронизирована со всеми последующими чтениями этой переменной. Такие переменные чаще всего используются для простых флагов, или при написании неблокирующих алгоритмов.
Еще одним эффектом использования volatile
-переменных является то, что чтение и запись гарантированно будут атомарными, что расширяет базовую гарантию атомарности на типы long
и double
.
Приведем еще некотрые действия синхронизация, поддерживающие реализацию параллелизма:
-
Запуск нового потока синхронизируется с его первым выполняемым действием. Это гарантирует то, что вновь запущенному потоку будут видны все данные заданные при его создании, включая его собственные поля.
-
Последнее, выполняемое потоком, действие синхронизировано со всеми действиями, фиксирующими его останов, такими как вызов
isAlive
илиjoin
. -
Прерывание потока синхронизировано со всеми фиксирующими его действиями, такими как возбуждение
InterruptedException
или вызов в другом потоке методаisInterrupted
данного потока. -
Запись значений переменных по-умолчанию (таких как
0
,null
илиfalse
) синхронизирована с первым действием в любом потоке. Это гарантирует, что даже в некорректно синхронизированной программе поток никогда не увидит произвольных значений переменной - они будут содержать либо значения, записанные каким-то другим потоком, либо значение по-умолчанию.
Существует дополнительные правила модели памяти, которые касаются использование final
полей:
-
Если ссылка на объект сохраняется после его создания, то любой поток, читающий эту ссылку, гарантировано будет видеть проинициализированные значения
final
-полей этого объекта. Относительно неfinal
-полей, читаемых без синхронизации, такой гарантии нет. -
При чтении ссылки из
final
-поля все неfinal
поля ссылаемого объекта будут содержать значения, по крайней мере, не ранее тех, что они содержали при её записи. Это, например, значит, что если поток создает объект и использует методы этого объекта для установки его значений, то второй поток будет видеть состояние, установленное первым поток, в том случае, если ссылка, используемая для доступа к этому объекту, после сделанных изменений была записана вfinal
поле. Всеfinal
поля этого объекта подчиняются первому правилу.
Использование синхронизированных методов, блоков синхронизации и volatile
-переменных предоставляет гарантии записи и чтения переменных вне самих таких переменных и блоков. Такие действия синхронизации обеспечивают то, что называется отношением выполняется прежде (happens before). Это отношение является транзитивным и выражение, стоящее перед каким-то другим в программном порядке, выполняется прежде этого второго выражения, что позволяет синхронизировать между потоками не синхронизированные действия. Например, если значение не volatile
-переменной было записано перед volatile
-переменной, а в другом потоке значение этой же volatile
-переменной читается перед чтением не volatile
-переменной, тогда запись не volatile
-переменной выполняется прежде её чтения, и гарантированно будет возвращенно записанное значение.
То же самое справедливо при использовании вместо volatile
-переменной синхронизированных методов доступа.
11. Управление потоками, безопасность, и ThreadGroup
При работе с потоками некоторые из них создаются библиотечным кодом. Для управления наборами потоков как целым и задания общих ограничений было бы удобно организовать их в соответствующие группы.
Потоки организуются в группы потоков (thread groups) для целей управления и безопасности. Группа потоков может содержать другую группу потоков составляя иерархию корнем которой является системная группа. Потоками в рамках группы можно управлять как единым целым, например, одновременно прерывая (interrupting) все потоки в группе, или устанавливая ограничение на максимальный приоритет потока в группе. Группы потоков, также, могут определять области безопасности (security domain). Потоки в группе, в общем, могут изменять другие потоки в этой группе, включая потоки подчиненных групп. В этом контексте изменять означает вызывать методы которые могут повлиять на поведение потока, такие как изменение его приоритета или прерывание. Однако, приложение может установить политику безопасности, запрещающую изменение потоков в группе потоками других групп. Потокам различных групп, также, могут быть настроены различные разрешения на выполнение действий в рамках приложения, таких как I/O.
В общем случае, защищенные (security-sensitive) действия (такие как создание потока, контроль потоков, выполнение I/O операций, или завершение приложения) перед выполнением всегда проверяются установленными менеджерами безопасности. Если действие запрещено политиками безопасности, то возбуждается SecurityException
. По-умолчанию менеджеры безопасности не устанавливаются.
Каждый поток принадлежит к какой-то группе потоков. Каждая группа представлена объектом класса ThreadGroup
в котором заданы ограничения для потоков этой группы. Задать потоку группу можно передав её объект в конструкторе при его создании. По-умолчанию, если менеджером безопасности не предусмотрено другого, каждый новый поток помещается в группу создавшего его потока. При завершении потока его объект удаляется из группы и, при отсутствии других ссылок, можен быть удален GC.
У класса Thread
существует четыре конструктора, позволяющих задать потоку его группу.
Для предотвращения создания потока с произвольной группой, в случае если текущему потоку запрещено размещать потоки в данной группе, эти конструкторы могут возбудить исключение SecurityException
.
Заданная потоку группа не может быть изменена после его создания. Получение группы потока выполняется с помощью метода Thread::getThreadGroup
. Проверка доступности изменения потока выполняется с помощью метода Thread::checkAccess
. Данный метод возбуждает исключение SecurityException
если изменение запрещено, и просто возвращает управление в противном случае.
Группа потоков может быть демон-группой. Демон-группа автоматически разрушается (destroyed) когда становится пустой. Указание группе данного признака не влияет на содержащиеся в ней потоки и группы.
В случае задания группе максимального значения приоритета потоков (используя метод Thread::setMaxPriority
), все попытки указать потоку более высокий приоритет будут автоматически сброшены до заданного максимума. Данное ограничение применяется и к самой группе - все попытки указать группе более высокое, чем текущее, значение максимального приоритета будут отклонены.
Класс ThreadGroup
имеет следующие конструкторы и методы:
-
ThreadGroup(name: String)
- создает новый объект классаTreadGroup
. Родительской группой, в этом случае, будет группа текущего потока. -
ThreadGroup(parent: ThreadGroup, name: String)
- создает новый объект классаThreadGroup
с заданным именем в указанной группе. Еслиparent == null
возбуждаетсяNullPointerException
. -
getName(): String
- возвращает имя даннойThreadGroup
. -
getParent(): ThreadGroup
- возвращает родительскуюThreadGroup
илиnull
при её отсутствии (что может иметь место только для группы потоков верхнего уровня) -
setDaemon(daemon: boolean): void
- устанавливает статус демона для данной группы потоков. -
isDaemon(): boolean
- возвращает статус демона данной группы потоков. -
setMaxPriority(maxPriority: int): void
- устанавливает максимальный приоритет данной группы потоков. -
getMaxPriority(): int
- возвращает текущий максимальный приоритет данной группы потоков. -
parentOf(g: ThreadGroup): boolean
- проверяет, является ли данная группа потоков родительской по отношению к группеg
, или самой этой группой. Более удобно об этом думать как является частью, т.к. группа является частью самой себя. -
checkAccess(): void
- возбуждает исключениеSecurityException
если текущему потоку не разрешено изменять данную группу потоков. Иначе, просто возвращает управление. -
destroy(): void
- разрушает (destroy) данную группу потоков. Группа потоков не должна содержать потоков, т.к. иначе будет возбуждено исключениеIllegalThreadStateException
. Если группа содержит другие группы, то они тоже должны не содержать потоков. Вызов этого метода не разрушает потоки в группе.
Работа с содержимым группы осуществляется с помощью двух наборов методов: для потоков, и для групп.
-
activeCount(): int
- возвращает приблизительное число активных потоков в этой группе включая потоки содержащиеся во всех подгруппах. Приблизительное потому, что при возврате количества эти данные уже могут устареть. Активный поток - это такой для которого методThread::isAlive
возвращаетtrue
. -
enumerate(threadsInGroup: Thread[], recurse: boolean): int
- заполняет, в пределах размера, массивthreadsInGroup
ссылками на активные потоки, и возвращает количество сохраненных потоков. Еслиrecurse == false
, учитываются только потоки в этой группе. В противном случае включаются все потоки в иерархии групп. -
enumerate(threadsInGroup: Thread[]): int
- эквивалентенenumerate(threadsInGroup, true)
. -
int activeGroupCount()
- подобенactiveCount
, но подсчитывает группы, вместо потоков, во всех подгруппах. Активный (active) означает существующий. -
enumerate(groupsInGroup: ThreadGroup[], recurse: boolean): int
- подобен методуenumerate
для потоков, но заполняет массив ссылками на объектыThreadGroup
. -
enumerate(groupsInGroup: ThreadGroup[]): int
- эквивалентенenumerate(groupsInGroup, true)
.
Вызов метода interrupt
для группы приводит к вызову interrupt
для каждого потока в группе, включая потоки во всех подгруппах.
Класса Thread
включает два статических метода для работы с группой текущего потока:
-
activeCount(): int
- возвращает число активных потоков в группе текущего потока. -
enumerate(threadsInGroup: Thread[]): int
- эквивалентен вызову методаenumerate(threadsInGroup)
объекта группы текущего потока.
Класс ThreadGroup
, также, поддерживает метод, который вызывается при завершении потока из-за неперехваченного исключения:
uncaughtException(thr: Thread, exc: Throwable): void
12. Потоки и исключения
Исключения всегда возникают в конкретном потоке вследствие его действий. Такие исключения являются синхронными и всегда остаются в самом потоке.
В случае если исключение не перехватывается, то поток прерывается. Для отслеживания таких исключений каждый поток может иметь заданную для него сущность UncaughtExceptionHandler
. UncaughtExceptionHandler
это вложенный в класс Thread
интерфейс в котором объявлен единственный метод:
uncaughtException(thr: Thread, exc: Throwable): void
который будет вызван если поток thr
прерван из-за исключения exc
.
Обработчик неперхваченных исключений потока задается используя метод Thread::setUncaughtExceptionHandler
.
Метод getUncaughtExceptionHandler
вернет явно заданный методом setUncaughtExceptionHandler
обработчик, или, если он не был задан, группу потока (ThreadGroup
). Если поток был завершен и не имеет группу, getUncaughtExceptionHandler
может вернуть null
.
Стандартная реализация метода uncaughtException
класса ThreadGroup
делегирует вызов родительской группе, при её наличии. В противном случае, используя статический метод Thread::getDefaultUncaughtExceptionHandler
, у системы запрашивается перехватчик по-умолчанию. Если такой перехватчик существует, то вызывается его метод uncaughtException
, иначе у исключения вызывается метод Throwable::printStackTrace
. Если исключение является объектом класса ThreadDeath
, то ничего не выполняется.
Приложение может контролировать обработку неперехваченных исключений как собственных, так и создаваемых библиотечным кодом, потоков на трех уровнях:
-
на системном уровне, заданием обработчика по-умолчанию с помощью статического метода
Thread::setDefaultUncaughtExceptionHandler
. Эта операция проверяется системой безопасности на возможность выполнения. -
на уровне групп потоков, расширяя класс
ThreadGroup
и переопределяя его методuncaughtException
. -
на уровне потока, с помощью метода
Thread::setUncaughtExceptionHandler
.
Существует две формы асинхронных исключений: внутренние ошибки JVM и исключения, причиной которых является вызов устаревшего (deprecated) метода Thread::stop
.
Вызов этого метода провоцирует возбуждение асинхронного исключения ThreadDeath
в потоке для которого он вызван. Нет ничего особенного в данном исключении, и оно может быть перехвачено как и любое другое.
Основной целью метода Thread::stop
было завершение потока контролируемым образом. Но эта цель не была достигнута по двум причинам. Во-первых, данное исключение не может гарантировать завершение потока, т.к. может быть перехвачено и проигнорировано. Во-вторых, его возбуждение в момент нахождения потока в критической области (critical section) может привести объект в несогласованное состояние из-за неожиданного освобождения блокировки. Вместо этого следует использовать метод Thread::interrupt
.
Вторая форма метода Thread::stop
принимает объект целевого исключения в качестве аргумента. Это является еще более хитрым трюком, так как может привести к тому, что проверяемое исключение будет возбуждено методом в котором оно не объявлено.
Для любого живого (alive) потока можно получить его текущий стек-трейс (stack trace). Метод Thread::getStackTrace
возвращает массив объектов класса StackTraceElement
, где нулевой элемент представляет текущий выполняемый метод.
Стек-трейс всех потоков в системе можно получить используя статический метод Thread::getAllStackTraces
, который возвращает отображение (map) каждого потока на соответствующий массив объектов StackTraceElement
.
Виртуальная машина не обязана возвращать не нулевые стек-трейсы для каждого потока, и ей также разрешено скрывать информацию о вызове некоторых методов.
13. Класс ThreadLocal
Класс ThreadLocal
позволяет иметь одну логическую переменную содержащую, при этом, независимые значения для каждого отдельного потока. Класс ThreadLocal
имеет метод set
для задания переменной значения в текущем потоке, и метод get
для получения её значения в текущем потоке.
Начальное значение может быть задано либо с помощью переопределения метода ThreadLocal::initialValue
, который по-умолчанию возвращает null
, либо передачей объекта класса Supplier
статическому методу ThreadLocal::withInitial
.
Сброс значения ThreadLocal
-переменной выполняется вызовом метода ThreadLocal::remove
. При последующем вызове метода Thread::get
, для определения возвращаемого значения, повторно будет вызван метод ThreadLocal::initialValue
.
При завершении потока заданное ThreadLocal
-переменной значение, в случае остутствия других ссылок, будет уничтожено GC.
При создании нового потока его ThreadLocal
-переменные будут содержать значения возвращенные методом initialValue
. Для унаследования новым потоком значений переменных текущего потока можно воспользоваться классом InheritableThreadLocal
. Класс InheritableThreadLocal
является подклассом ThreadLocal
и переопределяет метод childValue
для получения начальных значений переменных нового потока. Реализация по-умолчанию просто возвращает родительское значение.
При работе с пулами потоков (thread pooling) ThreadLocal
-переменные следует использовать осторожно, т.к. в этом случае один и тот же поток может быть задействован более одного раза.
14. Отладка потоков
Для помощи в отладке (debugging) многопоточного приложения класс Thread
имеет несколько вспомогательных методов:
-
toString(): String
- возвращает строковое представление потока, включая его имя, приоритет, и имя группы потоков. -
long getId(): long
- возвращает положительное значение, однозначно идентифицирующее живой поток. -
getState(): Thread.State
- возвращает текущее состояние потока.Thread.State
- это вложенное перечисление (enum
), в котором определены следующие константы:NEW
,RUNNABLE
,BLOCKED
,WAITING
,TIMED_WAITING
, иTERMINATED
. Вновь созданный поток имеет состояниеNEW
до тех пор пока не будет запущен и перейдет в состояниеRUNNABLE
, в котором будет находится до завершения, пока не перейдет вTERMINATED
. Во время работы до завершения поток может находиться в состоянииRUNNABLE
,BLOCKED
(будучи заблокированным, например при захвате блокировки монитора),WAITING
(в случае вызоваwait
), илиTIMED_WAITING
(в случае вызоваwait
с таймаутом). -
static dumpStack(): void
- печатает стек-трейс (stack trace) текущего потока вSystem.err
.
Для отслеживания состояния группы потоков можно воспользоваться следующими методами класса ThreadGroup
:
-
toString(): String
- возвращает строковое представление объекта классаThreadGroup
включая его имя и приоритет. -
list(): void
- рекурсивно выводит содержимое (результатtoString
у групп и потоков) данной группы потоков вSystem.out
.