- •Многопоточность
- •Приоритеты потоков
- •Класс Thread и интерфейс Runnable
- •Главный поток
- •Расширение Thread
- •Выбор подхода
- •Использование методов isAlive() и join()
- •Приоритеты потоков
- •Синхронизация
- •Использование синхронизированных методов
- •Прерывание потоков
- •Оператор synchronized
- •Использование класса ReentrantLock.
Прерывание потоков
Поток прекращает работу, когда его метод run возвращает управление. В JDK 1.0 использовался метод stop() , который вызывался из одного потока для завершения работы другого. В настоящее время он не рекомендован к применению.
В текущей версии Java не предусмотрено никакого способа принудительного прекращения работы потока. Вместо этого применяется метод interrupt(), который осуществляет запрос на прекращение работы потока.
Когда вызывается метод interrupt(), устанавливается статус прерывания (interrupted status) для потока. Это флаг типа Boolean, который обязательно присутствует в любом потоке. В процессе работы поток должен периодически проверять, следует ли ему прекратить выполнение.
Для того чтобы выяснить, установлен ли статус прерывания, необходимо получить объект потока, вызывав метод Thread.currentThread(), а затем обратиться к методу isInterrupted().
while (!Thread.currentThread().isInterrupted() && дополнительные проверки)
{
требуемые действия
}
Однако, если выполнение потока заблокировано, он не может проверить, следует ли завершить работу. В этом случае на помощь приходит исключение InterruptedException. Когда для блокированного объекта вызывается метод interrupt(), работа блокирующего вызова (метода sleep() или wait()) завершается и управление передается обработчику исключения InterruptedException.
В языке Java нет обязательного требования о завершении работы прерываемого потока. Прерывание просто "привлекает внимание" потока, а последний решает, как отреагировать. Для этого в теле блока catch определяются действия, предпринимаемые при возникновении исключения InterruptedException. Для некоторых наиболее важных потоков исключение можно игнорировать и продолжать работу. Но в большинстве случаев поток интерпретирует исключение как запрос на прекращение выполнения.
Синхронизация
Когда несколько потоков нуждаются в доступе к разделяемому ресурсу, им необходим некоторый способ гарантии того, что ресурс будет использоваться одновременно только одним потоком. Процесс, с помощью которого это достигается, называется синхронизацией. Java обеспечивает поддержку синхронизации — на уровне языка.
Начиная с JDK 5.0, в распоряжении разработчиков есть два механизма защиты фрагментов кода от одновременного обращения различных потоков, это: ключевое слово synchronized и класс ReentrantLock.
Синхронизировать код с помощью synchronized можно двумя способами, первый из которых - это использование синхронизированных методов.
Использование синхронизированных методов
Для защиты фрагментов кода от одновременного обращения различных потоков вызывают метод, который модифицирован ключевым словом synchronized. Пока поток находится внутри синхронизированного метода, все другие потоки, пытающиеся вызвать его (или любой другой синхронизированный метод) на том же самом экземпляре объекта, блокированы и должны ждать. При выходе потока из синхронизированного метода (такой участок кода в некоторых источниках называется монитором) блокирование прекращается и следующий ожидающий поток, может войти в вызванный метод.
Чтобы понять потребность в синхронизации, начнем с простого примера, который ее не использует, хотя должен. Следующая программа имеет три простых класса. Первый, Callme, содержит одиночный метод с именем call(). Метод call() имеет параметр типа string с именем msg. Этот метод пытается печатать строку msg внутри квадратных скобок. После того, как call() печатает открывающую скобку и строку msg, он вызывает Thread.sleep(1000), приостанавливающий текущий поток на одну секунду.
Пример в файле UnSynch.java.
// Эта программа не синхронизирована.
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Прерывание");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}
public void run() {
target.call(msg);
}
}
class UnSynch {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Привет");
Caller ob2 = new Caller(target, "Синхронизированный");
Caller ob3 = new Caller(target, "Мир");
// ждать завершения потоков
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Прерывание");
}
}
}
Вывод этой программы:
[Привет[Синхронизированный[Мир]
]
]
Вызывая sleep(), метод call() разрешает переключать выполнение на другой поток. Это приводит к выводу перепутанных строк сообщений. В данной программе нет возможности предохранить три потока от вызова одного и того же метода на одном и том же объекте в одно и то же время. Это известно как состояние состязаний (гонок), потому что три потока состязаются друг с другом, чтобы завершить метод. Представленный пример использовал метод sleep(), чтобы сделать эффекты повторяемыми и очевидными. В большинстве ситуаций состояние состязания более тонко и менее предсказуемо, потому что вы не можете точно знать, когда произойдет переключение контекста. Это может заставить программу некоторое время выполняться правильно, а затем — не правильно.
Чтобы стабилизировать предшествующую программу, нужно сериализовать (преобразовать в последовательную форму) доступ к методу call(). То есть требуется организовать доступ к нему только одного потока одновременно. Для этого нужно просто в начало определения метода call() вставить ключевое слово synchronized, например, как в следующем фрагменте:
class Callme {
synchronized void call(String msg) {
Это предохраняет потоки от входа в call(), пока его использует другой поток. После добавления synchronized вывод программы становится таким:
[Привет]
[Синхронизированный]
[Мир]
(Текст программы в файле Synch.java)
Всякий раз, когда имеется метод (или группа методов), который управляет внутренним состоянием объекта в многопоточной ситуации, нужно использовать ключевое слово synchronized для предотвращения состязаний. Помните, как только поток вызывает синхронизированный метод на некотором экземпляре объекта, никакой другой поток не может вызвать никакой другой синхронизированный метод на том же самом экземпляре объекта. Однако не синхронизированные методы на этом экземпляре объекта остаются вызываемыми.
