Брюс Эккель - Философия Java3
Третий и четвертый классы примера повторяют первые два, только вместо класса Thread они используют интерфейс Runnable.
Класс ThreadMethod демонстрирует создание потока в методе. Вы вызываете метод, когда программа готова к запуску потока, а метод возвращает управление после запуска потока. Если поток выполняет только вспомогательные операции и не является фундаментальной частью класса, то этот способ, вероятно, удобнее и практичнее запуска потока из конструктора класса.
Присоединение к потоку
Любой поток может вызвать метод join(), чтобы дождаться завершения другого потока перед своим продолжением. Если поток вызывает t.join() для другого потока t, то вызывающий поток приостанавливается до тех пор, пока целевой поток t не завершит свою работу (когда метод t.isAlive() вернет значение false).
Вызвать метод join() можно также и с аргументом, указывающим продолжительность ожидания (в миллисекундах или в миллисекундах с наносекундами). Если целевой поток не закончит работу за означенный период времени, метод join() все равно вернет управление инициатору.
Вызов join() может быть прерван вызовом метода interrupt() для потока-инициатора, поэтому потребуется блок try-catch.
Все эти операции продемонстрированы в следующем примере:
// concurrency/Joining java
// Демонстрация joinO.
import static net mindview util Print *;
class Sleeper extends Thread {
private int duration,
public Sleeper(String name, int sleepTime) { super(name), duration = sleepTime, startO,
}
public void run О {
try {
sleep(duration), } catchdnterruptedException e) {
print(getName() + " прерван " +
"isInterruptedO• " + islnterrupted()),
return;
}
print(getName() + " активизировался"),
}
}
class Joiner extends Thread { private Sleeper sleeper, public Joiner(String name. Sleeper sleeper) { super(name), this.sleeper = sleeper. startO;
}
public void runO { try {
sleeper joinO; } catchdnterruptedException e) { print("Прерван");
}
print (getNameO + " join завершен");
public class Joining {
public static void main(String[] args) { Sleeper
sleepy = new Sleeper("Sleepy". 1500), grumpy = new SIeeper("Grumpy". 1500).
Joiner
dopey = new Joiner("Dopey", sleepy), doc = new Joiner("Doc", grumpy); grumpy interruptO;
}
} /* Output-
Grumpy был прерван. isInterruptedO. false Doc join завершен Sleepy активизировался Dopey join завершен *///;-
Класс Sleeper — это тип потока, который приостанавливается на время, указанное в его конструкторе. В методе run() вызов метода sleep() может закончиться по истечении времени задержки, но может и прерваться. В секции catch выводится сообщение о прерывании, вместе со значением, возвращаемым методом islnterrupted(). Когда другой поток вызывает interrupt() для данного потока, устанавливается флаг, показывающий, что поток был прерван. Однако этот флаг сбрасывается при обработке исключения, поэтому внутри секции catch результатом всегда будет false. Флаг используется в других ситуациях, где поток может исследовать свое прерванное состояние в стороне от исключения.
Joiner — поток, который ожидает пробуждения потока Sleeper, вызывая для последнего метод join(). В методе main() каждому объекту Joiner сопоставляется Sleeper, и вы можете видеть в результатах работы программы, что, если Sleeper был прерван или завершился нормально, Joiner прекращает работу вместе с потоком Sleeper.
Совместное использование ресурсов
Однопоточную программу можно представить в виде одинокого работника, передвигающегося по своему пространству задачи и выполняющего по одной операции за один раз. Раз работник один, вам не приходится принимать во внимание проблему двух сущностей, пытающихся оспорить право на один и тот же ресурс, подобно двум людям, которые хотят одновременно поставить машину в одном месте, вдвоем пройти в одну дверь или даже говорить одновременно.
В условиях многозадачности ситуация меняется: у вас есть сразу два или три потока, которые стремятся получить доступ к одному и тому же ограниченному ресурсу. Если не предотвратить подобные конфликты, два потока могут попытаться получить доступ к одному счету в банке, одновременно распечатать два документа на одном принтере, изменить одно и то же значение, и т. п.
Некорректный доступ к ресурсам
Рассмотрим следующий пример, в котором одна задача генерирует четные числа, а другие задачи эти числа потребляют. Единственной задачей задач-потребите-лей является проверка четности этих чисел.
Начнем с определения EvenChecker, задачи-потребителя, поскольку она будет использоваться во всех последующих примерах. Чтобы отделить EvenChecker от различных генераторов, с которыми мы будем экспериментировать, мы определим абстрактный класс IntGenerator, содержащий минимум необходимых методов для EvenChecker: метод для получения следующего значения next() и методы отмены. Класс не реализует интерфейс Generator, потому что он должен выдавать int, а параметризация не поддерживает примитивные параметры.
//: concurrency/IntGenerator.java
public abstract class IntGenerator {
private volatile boolean canceled = false; public abstract int nextO; // Для отмены:
public void cancel О { canceled = true; } public boolean isCanceledO { return canceled; }
} III ~
IntGenerator содержит метод cancel(), изменяющий состояние флага canceled, и метод isCanceled(), проверяющий, был ли объект отменен. Поскольку флаг canceled относится к типу boolean, простые операции вроде присваивания и возврата значения выполняются атомарно, то есть без возможности прерывания, и вы не увидите поле в промежуточном состоянии между этими простыми операциями. Смысл ключевого слова volatile будет объяснен позже в этой главе.
Для тестирования IntGenerator можно воспользоваться следующим классом EvenChecker:
//. concurrency/EvenChecker.java import java.util.concurrent *,
public class EvenChecker implements Runnable { private IntGenerator generator, private final int id,
public EvenChecker(IntGenerator g. int ident) { generator = g. id = ident;
}
public void run О {
while({generator isCanceledO) {
int val = generator.nextO; if(val % 2 = 0) {
System.out.println(val + " не четно!"); generator cancel О; // Отмена всех EvenChecker
}
// Тестирование произвольного типа IntGenerator-public static void test(IntGenerator gp. int count) {
System out.println("Ha>KMme Control-С. чтобы завершить программу"), ExecutorService exec = Executors.newCachedThreadPoolО; for(int i = 0; i < count; i++)
exec.execute(new EvenChecker(gp, i)); exec.shutdownO;
}
// Значение по умолчанию для count: public static void test(IntGenerator gp) { test(gp. 10);
}
} ///-
Как видно из run(), все задачи EvenChecker, зависящие от объекта IntGenerator, проверяют, не были ли они отменены. При таком подходе задачи, совместно использующие общий ресурс (IntGenerator), наблюдают за ресурсом, ожидая от него сигнала завершения. Тем самым устраняется так называемая «ситуация гонки», когда две и более задачи торопятся отреагировать на некоторое условие; это приводит к возникновению конфликтов или получению других некорректных результатов. Будьте внимательны, постарайтесь продумать все возможные сбои в системах с параллельным выполнением и защититься от них. Например, задача не может зависеть от другой задачи, потому что порядок завершения задач не гарантирован. Зависимость задач от объекта, не являющегося задачей, устраняет потенциальную «ситуацию гонки».
Метод test() настраивает и тестирует произвольный тип IntGenerator, запуская несколько задач EvenChecker с общим IntGenerator. Если IntGenerator приводит к сбою, test() сообщает о происходящем и возвращает управление. В противном случае его следует завершить вручную клавишами Ctrl+C.
Задачи EvenChecker постоянно читают и проверяют значения, полученные от IntGenerator. Если generator.isCanceled() равен true, run() возвращает управление; это сообщает Executor в EvenChecker.test() о том, что задача завершена. Любая задача EvenChecker может вызвать cancel() для связанного с ней IntGenerator, в результате чего все остальные EvenChecker, использующие IntGenerator, будут корректно завершены. Как будет показано далее в этой главе, в Java существуют и более общие механизмы завершения потоков.
В первом варианте IntGenerator, который мы рассмотрим, next() выдает серию четных значений:
// concurrency/EvenGenerator java
// Конфликт потоков
public class EvenGenerator extends IntGenerator { private int currentEvenValue = 0, public int nextO {
++currentEvenValue. // Опасная точка!
++currentEvenValue;
return currentEvenValue,
}
public static void main(String[] args) {
EvenChecker test(new EvenGenerator()),
}
} /* Output
Нажмите Control-С, чтобы завершить программу
89476993 не четно!
89476993 не четно!
*/// ~
Одна задача может вызвать next() после того, как другая задача выполнит первый инкремент currentEvenValue, но до второго инкремента (в позиции, помеченной комментарием «Опасная точка!»). При этом значение оказывается в «некорректном» состоянии. Чтобы доказать, что такое возможно, EvenChecker. test() создает группу объектов EvenChecker, которые непрерывно читают результаты EvenGenerator и проверяют их на четность. При обнаружении нечетного числа выводится сообщение об ошибке, и программа завершается.
Сбой рано или поздно произойдет, потому что задачи EvenChecker могут обратиться к информации EvenGenerator в «некорректном» состоянии. Впрочем, проблема может проявиться лишь после многих циклов отработки EvenGenerator; все зависит от особенностей операционной системы и других подробностей реализации. Чтобы ускорить наступление сбоя, попробуйте разместить вызов yield() между инкрементами. В этом и состоит одна из проблем многопоточного программирования: программа, содержащая ошибку, на первый взгляд работает вполне нормально — а все потому, что вероятность сбоя очень мала.