Skip to content

Latest commit

 

History

History
1297 lines (1126 loc) · 110 KB

c11.md

File metadata and controls

1297 lines (1126 loc) · 110 KB

Глава 11

Многопоточное программирование

Основные навыки и понятия

  • Общее представление о многопоточной обработке
  • Класс Thread и интерфейс Runnable
  • Создание потока
  • Создание нескольких потоков
  • Определение момента завершения потока
  • Использование приоритетов потоков
  • Представление о синхронизации потоков
  • Применение синхронизированных блоков
  • Взаимодействие потоков
  • Приостановка, возобновление и остановка потоков

Среди многих замечательных свойств языка Java особое место принадлежит поддержке многопоточного программирования. Многопоточная программа состоит из двух или более частей, выполняемых параллельно. Каждая часть такой программы называется потоком и определяет отдельный путь выполнения команд. Таким образом, многопоточная обработка является особой формой многозадачности.

Общее представление о многопоточной обработке

Различают две разновидности многозадачности: на основе процессов и на основе потоков. В связи с этим важно понимать отличия между ними. Процесс фактически представляет собой исполняемую программу. Поэтому многозадачность на основе процессов — это средство, благодаря которому на компьютере могут параллельно выполняться две программы или более. Так, многозадачность на основе процессов позволяет одновременно выполнять программы текстового редактора, электронных таблиц и просмотра содержимого в Интернете. При организации многозадачности на основе процессов программа является наименьшей единицей кода, выполнение которой может координировать планировщик задач.

Поток представляет собой координируемую единицу исполняемого кода. Своим происхождением этот термин обязан понятию “поток исполнения”. При организации многозадачности на основе потоков у каждого процесса должен быть по крайней мере один поток, хотя их может быть и больше. Это означает, что в одной программе одновременно можно решать две и более задачи. Например, текст может форматироваться в редакторе текста одновременно с его выводом на печать, при условии, что оба эти действия выполняются в двух отдельных потоках. Несмотря на то что программы на Java выполняются в среде, поддерживающей многозадачность на основе процессов, в самих программах управлять процессами нельзя. В них можно управлять только потоками.

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

Как известно, за последние несколько лет широкое распространение нашли многопроцессорные или многоядерные вычислительные системы, хотя по-прежнему повсеместно используются и однопроцессорные системы. В этой связи следует иметь в виду, что языковые средства организации многопоточной обработки в Java пригодны для обеих разновидностей вычислительных систем. В одноядерной системе параллельно выполняющиеся потоки разделяют ресурсы одного ЦП, получая по очереди квант времени ЦП. Поэтому в одноядерной системе два или более потока на самом деле не выполняются параллельно, а лишь используют время простоя ЦП. С другой стороны, в многопроцессорных или многоядерных системах два потока или более могут выполняться параллельно. Это, как правило, позволяет повысить производительность программ и скорость выполнения отдельных операций.

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

В связи с организацией многозадачности на основе потоков возникает потребность в особого рода режиме, который называется синхронизацией и позволяет координировать выполнение потоков вполне определенным образом. Для такой синхронизации в Java предусмотрена отдельная подсистема, основные средства которой рассматриваются в этой главе.

Если вы пишете программы для таких операционных систем, как Windows, то принципы многопоточного программирования вам должны быть уже знакомы. Но то обстоятельство, что в Java имеются языковые средства для поддержки потоков, упрощает организацию многопоточной обработки, поскольку избавляет от необходимости реализовывать ее во всех деталях.

Класс Thread и интерфейс Runnable

В основу системы многопоточной обработки в Java положены класс Thread и интерфейс Runnable, входящие в пакет java. lang. Класс Thread инкапсулирует поток исполнения. Для того чтобы образовать новый поток, нужно создать класс, являющийся подклассом Thread или реализующий интерфейс Runnable.

В классе Thread определен ряд методов, позволяющих управлять потоками. Некоторые из этих наиболее употребительных методов описаны ниже. По мере их представления в последующих примерах программ вы ознакомитесь с ними поближе.

Метод Описание
final String getName() Получает имя потока
final int getPriority() Получает приоритет потока
final boolean isAliveO Определяет, выполняется ли поток
final void join() Ожидает завершения потока
void run() Определяет точку входа в поток
static void sleep(long миллисекунд) Приостанавливает исполнение потока на указанное число миллисекунд
void start() Запускает поток, вызывая его метод run()

В каждом процессе имеется как минимум один поток исполнения, который называется основным потоком. Он получает управление уже при запуске программы.

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

Создание потока

Для того чтобы создать поток, нужно построить объект типа Thread. Класс Thread инкапсулирует объект, который может стать исполняемым. Как пояснялось ранее, пригодные для исполнения объекты можно создавать в Java двумя способами:

  • реализуя интерфейс Runnable;
  • создавая подкласс класса Thread.

В большинстве примеров, представленных в этой главе, будет применяться первый способ. Хотя в примере для опробования 11.1 будет продемонстрировано, каким образом поток реализуется путем расширения класса Thread. Но независимо от выбранного способа создание экземпляра потока, организация доступа к нему и управление потоком осуществляется средствами класса Thread. Единственное отличие обоих способов состоит в том, как создается класс, активизирующий поток.

Интерфейс Runnable дает абстрактное описание единицы исполняемого кода. Для формирования потока подходит любой объект, реализующий этот интерфейс. В интерфейсе Runnable объявлен только один метод, run():

public void run()

В теле метода run() определяется код, соответствующий новому потоку. Из этого метода можно вызывать другие методы, использовать в нем различные классы и объявлять переменные таким же образом, как это делается в основном потоке. Единственное отличие состоит в том, что метод run() создает точку входа в поток, исполняемый в программе параллельно с основным. Этот поток исполняется до тех пор, пока не произойдет возврат из метода run().

После создания класса, реализующего интерфейс Runnable, следует создать экземпляр объекта типа Thread на основе объекта данного класса. В классе Thread определен ряд конструкторов. В дальнейшем будет использоваться следующий конструктор:

Thread(Runnable threadOb)

В качестве параметра threadOb этому конструктору передается экземпляр класса, реализующего интерфейс Runnable. Благодаря этому определяется место для исполнения потока.

Созданный поток не начнет исполнение до тех пор, пока не будет вызван метод start(), объявленный в классе Thread. По существу, единственным назначением метода start() является вызов метода run(). А объявляется метод start() следующим образом:

void start()

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

// Создание потока путем реализации интерфейса Runnable,
class MyThread implements Runnable {
    String thrdName;

    // Объекты типа MyThread выполняются в отдельных потоках, так как
    // класс MyThread реализует интерфейс Runnable.
    MyThread(String name) {
        thrdName = name;
    }

    // Точка входа в поток,
    public void run() {
        // Здесь начинают исполняться потоки.
        System.out.println(thrdName + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrdName +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrdName + " interrupted.");
        }
        System.out.println(thrdName + " terminating.");
    }
}

class UseThreads {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");
        // сначала построить объект типа MyThread
        MyThread mt = new MyThread("Child #1"); // Создание исполняемого объекта.
        // далее сформировать поток из этого объекта
        Thread newThrd = new Thread(mt); // Формирование потока из этого объекта.
        // и, наконец, начать исполнение потока

        newThrd.start О; // Начало исполнения потока.
        for(int i=0; i<50; i++) {
            System.out.print(".") ;
            try {
                Thread.sleep(100) ;
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        System.out.println("Main thread ending.");
    }
}

Рассмотрим исходный код приведенной выше программы более подробно. Как видите, класс MyThread реализует интерфейс Runnable. Это означает, что объект типа MyThread подходит для использования в качестве потока, а следовательно, его можно передать конструктору класса Thread.

В теле метода run() присутствует цикл, в котором производится отсчет от 0 до 9. Обратите внимание на вызов метода sleep(). Этот метод приостанавливает поток, из которого он был вызван на указанное число миллисекунд. Ниже приведена общая форма объявления данного метода.

static void sleep(long миллисекунд) throws InterruptedException

Единственный параметр метода sleep() задает время задержки, определяемое числом миллисекунд. Как следует из объявления этого метода, в нем может быть сгенерировано исключение InterruptedException. Следовательно, его нужно вызывать в блоке try. Имеется и другой вариант метода sleep(), позволяющий точнее указывать время задержки в миллисекундах и дополнительно в наносекундах. Когда метод sleep() вызывается в методе run(), исполнение потока приостанавливается на 400 миллисекунд на каждом шаге цикла. Благодаря этому поток исполняется достаточно медленно, чтобы можно проследить за ним.

В методе main() создается новый объект типа Thread. Для этой цели служит приведенная ниже последовательность операторов.

// сначала построить объект типа MyThread
MyThread mt = new MyThread("Child #1");
// далее сформировать поток из этого объекта
Thread newThrd = new Thread(mt);
// и, наконец, начать исполнение потока
newThrd.start();

Как видите, сначала создается объект типа MyThread, а затем он используется для построения объекта типа Thread. Его можно передать конструктору класса Thread в качестве параметра, поскольку класс MyThread реализует интерфейс Runnable. И наконец, начинается исполнение нового потока, для чего вызывается метод start(), что приводит к вызову метода run() из порожденного потока. После вызова метода start() управление возвращается к методу main(), где начинается выполнение цикла for. Этот цикл повторяется 50 раз, приостанавливая на 100 миллисекунд исполнение потока на каждом своем шаге. Оба потока продолжают исполняться, разделяя ресурсы

ЦП в однопроцессорной системе до тех пор, пока циклы в них не завершатся. Ниже приведен результат выполнения данной программы. Вследствие отличий в вычислительных средах у вас может получиться несколько иной результат.

Main thread starting.
.Child #1 starting.
....In Child #1, count is 0
.....In Child #1, count is 1
.....In Child #1, count is 2
.....In Child #1, count is 3
.....In Child #1, count is 4
.....In Child #1, count is 5
.....In Child #b count is 6
.....In Child #1, count is 7
.....In Child #1, count is 8
.....In Child #1 count is 9
Child #1 terminating.
    Main thread ending

В рассматриваемом здесь первом примере организации многопоточной обработки любопытно также отметить следующее обстоятельство: для демонстрации того факта, что основной и порожденный потоки исполняются одновременно, необходимо задержать завершение метода main() до тех пор, пока не окончится порожденный поток mt. В данном примере это достигается благодаря отличиям во временных характеристиках обоих потоков. Вызовы метода sleep() из цикла for в методе main() приводят в итоге к задержке на 5 секунд (50 шагов цикла х 100 миллисекунд), тогда как общая задержка с помощью того же самого метода в аналогичном цикле в методе run() составляет лишь 4 секунды (10 шагов цикла х 400 миллисекунд). Поэтому метод run() завершится приблизительно на 1 секунду раньше, чем метод main(). В итоге основной и порожденный потоки будут выполняться параллельно до тех пор, пока не завершится порожденный поток mt. А приблизительно через одну секунду завершится и основной поток в методе main().

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

И последнее замечание: многопоточная программа обычно разрабатывается с таким расчетом, чтобы последним завершал свою работу основной поток. Как правило, выполнение программы продолжается до тех пор, пока все потоки не завершат работу. Поэтому завершение основного потока является не требованием, а рекомендуемой для наследования нормой, особенно для тех, кто лишь начинает осваивать многопоточное программирование.

Несложные усовершенствования многопоточной программы

Рассмотренная выше многопоточная программа вполне работоспособна, тем не менее ей не помешает небольшая доработка, повышающая ее эффективность. Во- первых, можно сделать так, чтобы поток начинал исполняться сразу после создания. Эта цель достигается созданием экземпляра объекта типа Thread в конструкторе класса MyThread. И во-вторых, нет никакой нужды хранить в объекте типа MyThread имя потока, но присвоить имя потоку при его создании. Эту задачу позволяет решить следующий вариант конструктора Thread:

Thread(Runnable threadOb, String имя)

где имя обозначает конкретное наименование потока.

Получить имя потока можно, используя метод getName(), определенный в классе Thread. Ниже приведено объявление этого метода.

final String getName()

В приведенной ниже программе имя присваивается потоку после его создания с помощью метода setName(). И хотя в этом нет особой необходимости, такое решение выбрано лишь для того, чтобы продемонстрировать возможности класса Thread. Объявление метода setName() имеет следующий вид:

final void setName(String имя_потока)

где имя_потока обозначает имя, которое присваивается потоку.

Ниже приведена видоизмененная версия предыдущей программы.

// Видоизменение класса MyThread.
class MyThread implements Runnable {
    Thread thrd; // В этой переменной хранится ссылка на поток.

    // построить новый поток
    MyThread(String name) {
        thrd = new Thread(this, name); // Поток именуется при его создании,
        thrd.start() ; // Начало исполнения потока.
    }

    // начать исполнение нового потока
    public void run()   {
        System.out.println(thrd.getName() + " starting.");
        try {
            for (int count=0; countclO; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrd.getName() +
                                   ", count is " + count)';
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " terminating.");
    }
}

class UseThreadsImproved {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");

        // Теперь поток начинается при его создании.
        MyThread mt = new MyThread("Child #1");

        for (int i=0; i < 50; i++) {
            System.out.print(".") ;
            try {
                Thread.sleep(100) ;
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        System.out.println("Main thread ending.");
    }
}

Эта версия программы дает такой же результат, как и предыдущая. Обратите внимание на то, что ссылка на поток хранится в переменной thrd экземпляра класса MyThread.

Пример для опробования 11.1. Расширение класса Thread

Реализация интерфейса Runnable — это лишь один из способов получения экземпляров потоковых объектов. Другой способ состоит в создании подкласса, производного от класса Thread. В этом проекте будет продемонстрировано, каким образом расширение класса Thread позволяет реализовать такие же функциональные возможности, как и у рассмотренной выше программы UseThreadsImproved.

В подклассе, производном от класса Thread, нужно переопределить метод run(), который является точкой входа в новый поток. Для того чтобы начать исполнение нового потока, следует вызвать метод start(). Можно также переопределить и другие методы из класса Thread, но делать это не обязательно.

Последовательность действий

  1. Создайте файл ExtendThread.java. Скопируйте в этот файл исходный код второго рассмотренного ранее примера программы (файл UseThreadsImproved. java).
  2. Измените объявление класса MyThread. Теперь он должен быть подклассом, производным от класса Thread, как показано ниже.
    class MyThread extends Thread {
    
  3. Удалите следующую строку кода:
    Thread thrd;
    
    Переменная thrd уже не нужна, поскольку класс MyThread включает в себя экземпляр класса Thread и может ссылаться на самого себя.
  4. Внесите в конструктор класса Thread следующие изменения:
    // построить новый поток.
    MyThread(String name) {
        super(name); // присвоить потоку имя
        start(); // начать поток
    }
    
    Как видите, в данном конструкторе присутствует ключевое слово super, которое используется для вызова следующего варианта конструктора Thread:
    Thread(String имя);
    
    где имя обозначает присваиваемое потоку конкретное имя.
  5. Внесите приведенные ниже изменения в метод run(), чтобы он вызывал метод getName() непосредственно, не предваряя его именем переменной thrd.
    // начать исполнение нового метода
    public void run()  {
        System.out.println(getName() + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + getName() +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(getName() + " interrupted.");
        }
    
        System.out.println(getName() + " terminating.");
    }
    
  6. Ниже приведен весь исходный код программы, в которой вместо реализации интерфейса Runnable используется подкласс, производный от класса Thread. Выполнение этой программы дает такой же результат, как и предыдущие ее версии.
    /*
    Пример для опробования 11.1.
    Расширение класса Thread.
    */
    class MyThread extends Thread {
        // построить новый поток
        MyThread(String name) {
            super(name); // присвоить потоку  имя
            start(); // начать поток
        }
    
        // начать исполнение нового потока
        public void run() {
            System.out.println(getName() + " starting.");
            try {
                for(int count=0; count < 10; count++) {
                    Thread.sleep(400);
                    System.out.println("In " + getName() +
                                       ", count is " + count);
                }
            }
            catch(InterruptedException exc) {
                System.out.println(getName() + " interrupted.");
            }
            System.out.println(getName() + " terminating.");
        }
    }
    
    class ExtendThread {
        public static void main(String args[])  {
            System.out.println("Main thread starting.");
            MyThread mt = new MyThread("Child #1");
            for(int i=0; i < 50; i++) {
                System.out.print(".");
                try {
                    Thread.sleep(100);
                }
                catch(InterruptedException exc) {
                    System.out.println("Main thread interrupted.");
                }
            }
            System.out.println("Main thread ending.");
        }
    }
    

Создание нескольких потоков

В предыдущем примере был создан только один порожденный поток. Но в программе можно породить столько потоков, сколько требуется. Например, в приведенной ниже программе формируются три порожденных потока.

// Создание нескольких потоков.
class MyThread implements Runnable {
    Thread thrd;
    // построить новый поток
    MyThread(String name) {
        thrd = new Thread(this, name);

        thrd.start(); // начать поток
    }
    // начать исполнение нового потока
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrd.getName() +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " terminating.");
    }
}

class MoreThreads {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");

        // Создание и запуск на исполнение трех потоков.
        MyThread mtl = new MyThread("Child #1");
        MyThread mt2 = new MyThread("Child #2");
        MyThread mt3 = new MyThread("Child #3");

        for (int i=0;   i   < 50; i++) {
            System.out.print(".");
            try {
                Thread.sleep(100);
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        System.out.println("Main thread ending.");
    }
}

Ниже приведен результат выполнения данной программы.

Main thread starting.
Child #1 starting.
.Child #2 starting.
Child #3 starting.
...In Child #3, count is О
In Child #2, count is 0
In Child #1, count is 0
....In Child #1, count is 1
In Child #2, count is 1
In Child #3, count is 1
....In Child #2, count is 2
In Child #3, count is 2
In Child #1, count is 2
...In Child #1, count is 3
In Child #2, count is 3
In Child #3, count is 3
....In Child #1, count is 4
In Child #3, count is 4
In Child #2, count is 4
....In Child #1, count is 5
In Child #3, count is 5
In Child #2, count is 5
...In Child #3, count is 6
.In Child #2, count is 6
In Child #1, count is 6
...In Child #3, count is 7
In Child #1, count is 7
In Child #2, count is 7
....In Child #2, count is 8
In Child #1, count is 8
In Child #3, count is 8
....In Child #1, count is 9
Child #1 terminating.
In Child #2, count is 9
Child #2 terminating.
In Child #3, count is 9
Child #3 terminating.
    Main thread ending.

Как видите, после запуска на исполнение все три потока совместно используют ресурсы ЦП. Следует иметь в виду, что потоки в данном примере запускаются на исполнение в том порядке, в каком они были созданы. Но так происходит не всегда. Исполняющая система Java сама планирует исполнение потоков. Вследствие отличий в вычислительных средах у вас может получиться несколько иной результат.

Определение момента завершения потока

Нередко требуется знать, когда завершится поток. Так, в приведенных выше примерах ради большей наглядности нужно было поддерживать основной поток действующим до тех пор, пока не завершатся остальные потоки. Для этой цели основной поток переводился в состояние ожидания на более продолжительное время, чем порожденные им потоки. Но такое решение вряд ли можно считать удовлетворительным или общеупотребительным!

Правда, в классе Thread предусмотрены два средства, позволяющие определить, завершился ли поток. Первым из них является метод is Alive(), объявление которого приведено ниже.

final boolean isAlive()

Этот метод возвращает логическое значение true, если поток, для которого он вызывается, все еще исполняется. В противном случае он возвращает логическое значение false. Для того чтобы опробовать метод isAlive() на практике, замените в предыдущей программе класс MoreThreads новой версией, исходный код которой приведен ниже.

// Применение метода isAlive().
class MoreThreads {
    public static void main(String args[])  {
        System.out.println("Main thread starting.");

        MyThread mtl = new MyThread("Child #1");
        MyThread mt2 = new MyThread("Child #2");
        MyThread mt3 = new MyThread("Child #3");

        do {
            System.out.print(" . ") ;
            try {
                Thread.sleep(100);
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
            // Ожидание завершения потоков.
        } while (mtl.thrd.isAlive() ||
                 mt2.thrd.isAlive() ||
                 mt3.thrd.isAlive());
        System.out.println("Main thread ending.");
    }
}

Эта версия дает такой же результат, как и предыдущая. Единственное отличие состоит в том, что в данном случае ожидание завершения порожденного потока организовано с помощью метода isAlive(). Вторым средством, позволяющим определить, завершился ли поток, является метод join(), объявление которого приведено ниже.

final void join() throws InterruptedException

Этот метод ожидает завершения потока, для которого он был вызван. Его имя join выбрано потому, что вызывающий поток ожидает, когда указанный поток присоединится (англ.уши) к нему. Имеется и другой вариант метода j oin(), позволяющий указать максимальное время ожидания момента, когда поток завершится.

В приведенном ниже примере программы наличие метода join() гарантирует, что основной поток завершит работу последним.

// Применение метода join().
class MyThread implements Runnable {
    Thread thrd;

    // построить новый поток
    MyThread(String name) {
        thrd = new Thread(this, name);
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrd.getName() +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " terminating.");
    }
}

class JoinThreads {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");

        MyThread mtl = new MyThread("Child #1");
        MyThread mt2 = new MyThread("Child #2");
        MyThread mt3 = new MyThread("Child #3");

        try {
            // Ожидание до тех nop, пока указанный метод не завершится.
            mtl.thrd.join();
            System.out.println("Child #1 joined.");
            mt2.thrd.join() ;
            System.out.println("Child #2 joined.");
            mt3.thrd.join();
            System.out.println("Child #3 joined.");
        }
        catch(InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread ending.");
    }
}

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

Main thread starting.
Child #1 starting.
Child #2 starting.
Child #3 starting.
In Child #2, count is 0
In Child #1, count is 0
In Child #3, count is 0
In Child #2, count is 1
In Child #3, count is 1
In Child #1, count is 1
In Child #2, count is 2
In Child #1, count is 2
In Child #3, count is 2
In Child #2, count is 3
In Child #3, count is 3
In Child#1, count is 3
In Child #3, count is 4
In Child #2, count is 4
In Child #1, count is 4
In Child #3, count is 5
In Child #1, count is 5
In Child #2, count is 5
In Child #3, count is 6
In Child #2, count is 6
In Child #1, count is 6
In Child #3, count is 7
In Child #1, count is 7
In Child #2, count is 7
In Child #3, count is 8
In Child #2, count is 8
In Child #1, count is 8
In Child #3, count is 9
Child #3 terminating.
In Child #2, count is 9
Child #2 terminating.
In Child #1, count is 9
Child #1 terminating.
Child #1 joined.
Child #2 joined.
Child #3 joined.
Main thread ending.

Как видите, после того как вызываемый метод j oin() возвращает управление, исполнение потока прекращается.

Приоритеты потоков

У каждого потока имеется свой приоритет, который отчасти определяет, насколько часто поток получает доступ к ЦП. Вообще говоря, низкоприоритетные потоки получают доступ к ЦП реже, чем высокоприоритетные. Таким образом, в течение заданного промежутка времени низкоприоритетному потоку будет доступно меньше времени ЦП, чем высокоприоритетному. Как и следовало ожидать, время ЦП, получаемое потоком, оказывает определяющее влияние на характер его исполнения и взаимодействия с другими потоками, исполняемыми в настоящий момент в системе.

Следует иметь в виду, что, помимо приоритета, на частоту доступа потока к ЦП оказывают влияние и другие факторы. Так, если высокоприоритетный поток ожидает доступа к некоторому ресурсу, например для ввода с клавиатуры, он блокируется, а вместо него исполняется низкоприоритетный поток. Но когда высокоприоритетный поток получит доступ к ресурсам, он прервет низкоприоритетный поток и возобновит свое исполнение. На планирование работы потоков оказывает также влияние способ, посредством которого в операционной системе поддерживается многозадачность (см. врезку “Обращение к знатоку” в конце этого раздела). Следовательно, если один поток имеет более высокий приоритет, чем другой поток, это еще не означает, что первый поток будет исполняться быстрее второго. Высокий приоритет потока лишь означает, что потенциально он может получить больше времени ЦП.

При запуске порожденного потока его приоритет устанавливается равным приоритету родительского потока. Изменить приоритет можно, вызвав метод setPriority() из класса Thread. Ниже приведено объявление этого метода,

final void setPriority(int уровень)

В качестве параметра уровень данному методу передается новый приоритет для потока. Значение параметра уровень должно находиться в пределах от MIN PRIORITY до MAX PRIORITY. В настоящее время этим константам соответствуют числовые значения от 1 до 10. Для того чтобы восстановить приоритет потока по умолчанию, следует указать значение 5, которому соответствует константа N0RM PRI0RITY. Константы, определяющие приоритеты потоков, определены как static final в классе Thread.

Получить текущий приоритет можно с помощью метода getPriorityO из класса Thread, объявляемого следующим образом:

final int getPriority()

Ниже приведен пример программы, демонстрирующий использование двух потоков с разными приоритетами. Потоки создаются как экземпляры класса Priority. В методе run() содержится цикл, отсчитывающий число своих шагов. Этот цикл завершает работу, когда значение счетчика достигает 10000000 или же когда статическая переменная stop принимает логическое значение true. Первоначально переменной stop присваивается логическое значение false, но первый же поток, заканчивающий отсчет, устанавливает в ней логическое значение true. В результате второй поток завершится, как только ему будет выделен квант времени. В цикле производится проверка символьной строки в переменной currentName на совпадение с именем исполняемого потока. Если они не совпадают, это означает, что произошло переключение задач. При этом отображается имя нового потока, которое присваивается переменной currentName. Это дает возможность следить за тем, насколько часто каждый поток получает время ЦП. После остановки обоих потоков выводится число шагов, выполненных в каждом цикле.

// Демонстрация потоков с разными приоритетами.
class Priority implements Runnable {
    int count;
    Thread thrd;

    static boolean stop = false;
    static String currentName;

    /* Построение нового потока. Обратите внимание на то,
       что конструктор не запускает поток на исполнение. */
    Priority(String name) {
        thrd = new Thread(this, name);
        count = 0;
        currentName = name;
    }

    // начать исполнение нового потока
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        do {
            count++;

            if(currentName.compareTo(thrd.getName())    !=  0)  {
                currentName = thrd.getName();
                System.out.println("In " + currentName);
            }
        // Первый же поток, в котором достигнуто значение 10000000,
        // завершает остальные потоки.
        } while(stop == false && count < 10000000);
        stop = true;

        System.out.println("\n" + thrd.getName() +
                           " terminating.");
    }
}

class PriorityDemo {
    public static void main(String args[]) {
        Priority mtl = new Priority("High Priority");
        Priority mt2 = new Priority("Low Priority");

        // задать приоритеты
        // Поток mtl получает более высокий приоритет, чем поток mt2.
        mtl.thrd.setPriority(Thread.NORM_PRIORITY+2);
        mt2.thrd.setPriority(Thread.NORM_PRIORITY-2);

        // запустить потоки на исполнение
        mtl.thrd.start();
        mt2.thrd.start();

        try {
            mtl.thrd.join();
            mt2.thrd.join();
        }
        catch(InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("\nHigh priority thread counted to " +
        mtl.count);
        System.out.println("Low priority thread counted to " +
        mt2.count);
    }
}

Результат выполнения данной программы выглядит следующим образом:

High Priority starting.
In High Priority
Low Priority starting.
In Low Priority
In High Priority

High Priority terminating.

Low Priority terminating.

High priority thread counted to 10000000
Low priority thread counted to 8183

В данном примере большую часть времени ЦП получает высокоприоритетный поток. Очевидно, что результат выполнения программы существенно зависит от быстродействия ЦП и их количества, типа операционной системы и наличия прочих задач, выполняющихся в системе.

Синхронизация

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

Главным для синхронизации в Java является понятие монитора, контролирующего доступ к объекту. Монитор реализует принцип блокировки. Если объект заблокирован одним потоком, то он оказывается недоступным для других потоков. В какой-то момент объект разблокируется, и другие потоки могут обращаться к нему.

У каждого объекта в Java имеется свой монитор. Этот механизм встроен в сам язык. Следовательно, все объекты поддаются синхронизации. Для поддержки синхронизации в Java предусмотрено ключевое слово synchronized и ряд вполне определенных методов у каждого из объектов. А поскольку средства синхронизации встроены в язык, то пользоваться ими на практике очень просто — гораздо проще, чем может показаться на первый взгляд. Для многих программ средства синхронизации объектов по сути прозрачны.

Синхронизировать код можно двумя способами. Оба способа рассматриваются ниже, и в обоих используется ключевое слово synchronized.

Применение синхронизированных методов

Для того чтобы синхронизировать метод, в его объявлении следует указать ключевое слово synchronized. Когда такой метод получает управление, вызывающий поток активизирует монитор, что приводит к блокированию объекта. Если объект блокирован, он недоступен из другого потока, а кроме того, его нельзя вызвать из других синхронизированных методов, определенных в классе данного объекта. Когда выполнение синхронизированного метода завершается, монитор разблокирует объект, что позволяет другому потоку использовать этот метод. Таким образом, для достижения синхронизации программирующему на Java не приходится прилагать каких-то особых усилий.

Ниже приведен пример программы, демонстрирующий контролируемый доступ к методу sumArray(). Этот метод суммирует элементы целочисленного массива.

// Применение ключевого слова synchronize для управления доступом.
class SumArray {
    private int sum;

    // Метод sumArray() синхронизирован.
    synchronized int sumArray(int nums[]) {
        sum = 0; // обнулить сумму

        for(int i=0; i<nums.length; i++) {
            sum += nums[i];
            System.out.println("Running total for " +
                                Thread.currentThread().getName() +
                                " is " + sum);
            try {
                Thread.sleep(10); // разрешить переключение задач
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        return sum;
    }
}

class MyThread implements Runnable {
    Thread thrd;
    static SumArray sa = new SumArray();
    int a[];
    int answer;

    // построить новый поток
    MyThread(String name, int nums[]) {
        thrd = new Thread(this, name);
        a = nums;.
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        int sum;

        System.out.println(thrd.getName() + " starting.");

        answer = sa.sumArray(a);
        System.out.println("Sum for " + thrd.getName() +
                           " is " + answer);

        System.out.println(thrd.getName() + " terminating.");
    }
}

class Sync {
    public static void main(String args[]) {
        int a[] = {1, 2, 3, 4, 5};

        MyThread mtl = new MyThread("Child #1", a);
        MyThread mt2 = new MyThread("Child #2", a);
    }
}

Выполнение этой программы дает следующий результат:

Child #1 starting.
Running total for Child #1 is 1
Child #2 starting.
Running total for Child #1 is 3
Running total for Child #1 is 6
Running total for Child #1 is 10
Running total for Child #1 is 15
Sum for Child #1 is 15
Child #1 terminating.
Running total for Child #2 is 1
Running total for Child #2 is 3
Running total for Child #2 is 6
Running total for Child #2 is 10
Running total for Child #2 is 15
Sum for Child #2 is 15
Child #2 terminating.

Рассмотрим подробнее эту программу. В ней определены три класса. Имя первого — SumArray. В нем содержится метод sumArray(), вычисляющий сумму элементов целочисленного массива. Во втором классе MyThread используется статический объект sa типа SumArray для получения суммы элементов массива. А поскольку он статический, то все экземпляры класса MyThread используют одну его копию. И наконец, в классе Sync создаются два потока, в каждом из которых должна вычисляться сумма элементов массива.

В методе sumArray() вызывается метод sleep(). Он нужен лишь для того, чтобы обеспечить переключение задач. Метод sumArray() синхронизирован, и поэтому в каждый момент времени он может использоваться только одним потоком. Следовательно, когда второй порожденный поток начинает свое исполнение, он не может вызвать метод sumArray() до тех пор, пока этот метод не завершится в первом потоке. Благодаря этому обеспечивается правильность получаемого результата.

Для того чтобы лучше понять эффект от использования ключевого слова synchronized, попробуйте удалить его из объявления метода sumArray(). В итоге метод sumArray() потеряет синхронизацию и может быть использован в нескольких потоках одновременно. Это приведет к затруднению в связи с тем, что результат расчета суммы сохраняется в переменной sum, значение которой изменяется при каждом вызове метода sumArray() для статического объекта sa. Так, если в двух потоках одновременно сделать вызов sa. sumArray(), расчет суммы окажется неверным, поскольку в переменной sum накапливаются результаты суммирования, выполняемого одновременно в двух потоках. Ниже приведен результат выполнения той же самой программы, где из объявления метода sumArray() удалено ключевое слово synchronized. (Вследствие отличий в вычислительных средах у вас может получиться несколько иной результат.)

Child #1 starting.
Running total for Child #1 is 1
Child #2 starting
Running total for Child #2 is 1
Running total for Child #1 is 3
Running total for Child #2 is 5
Running total for Child #2 is 8
Running total for Child #1 is 11
Running total for Child #2 is 15
Running total for Child #1 is 19
Running total for Child #2 is 24
Sum for Child #2 : Is 24
Child #2 terminating.
Running total for Child #1 is 29
Sum for Child #1 : Ls 29
Child #1 terminating.

Нетрудно заметить, что вследствие одновременного вызова sa. sumArray() из разных потоков результат искажается.

Прежде чем переходить к рассмотрению следующей темы, перечислим основные свойства синхронизированных методов.

  • Синхронизированный метод создается путем указания ключевого слова synchronized в его объявлении.
  • Как только синхронизированный метод любого объекта получает управление, объект блокируется и ни один синхронизированный метод этого объекта не может быть вызван другим потоком.
  • Потоки, которым требуется синхронизированный метод, используемый другим потоком, ожидают до тех пор, пока не будет разблокирован объект, для которого он вызывается.
  • Когда синхронизированный метод завершается, разблокируется объект, для которого он вызывается.

Синхронизированные блоки

Несмотря на то что создание синхронизированных методов в классах — простой и эффективный способ управления потоками, такой способ оказывается пригодным далеко не всегда. Иногда возникает потребность синхронизировать доступ к методам, в объявлении которых отсутствует ключевое слово synchronized. Подобная ситуация часто возникает при использовании классов, которые были созданы независимыми разработчиками и исходный код которых недоступен. В таком случае ввести в объявление нужного метода ключевое слово synchronized вряд ли удастся. Как же тогда синхронизировать объект класса, содержащего этот метод? К счастью, данное затруднение разрешается очень просто. Достаточно ввести вызов метода в синхронизированный кодовый блок типа synchronized.

Синхронизированный блок определяется следующим образом:

synchronized{ссылка_на_объект) {
    // синхронизируемые операторы
}

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

Следовательно, обращение к методу sumArray() можно синхронизировать, вызвав его из синхронизированного блока. Такой способ демонстрируется в приведенной ниже переделанной версии предыдущей программы.

// Применение синхронизированного блока
// для управления доступом к методу sumArray().
class SumArray {
    private int sum;

    // Здесь метод sumArray() не синхронизирован.
    int sumArray(int nums[]) {
        sum =0; // обнулить сумму
        for(int i=0; icnums.length; i++) {
            sum += nums[i];
            System.out.println("Running total for " +
                               Thread.currentThread().getName() +
                               " is " + sum);
            try {
                Thread.sleep(10); // разрешить переключение задач
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        return sum;
    }
}

class MyThread implements Runnable {
    Thread thrd;
    static SumArray sa = new SumArray();
    int a[];
    int answer;

    // построить новый поток
    MyThread(String name, int nums[]) {
        thrd = new Thread(this, name);
        a = nums;
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        int sum;

        System.out.println(thrd.getName() + " starting.");

        // Здесь вызовы метода sumArray() для объекта sa синхронизированы.
        synchronized(sa) {
            answer = sa.sumArray(a);
        }
        System.out.println("Sum for " + thrd.getName() +
                           " is " + answer);
        System.out.println(thrd.getName() + " terminating.");
    }
}

class Sync {
    public static void main(String args[]) {
        int a [] = {1, 2, 3, 4, 5};

        MyThread mtl = new MyThread("Child #1", a);
        MyThread mt2 = new MyThread("Child #2", a);

        try {
            mtl.thrd.join();
            mt2.thrd.join();
        } catch (InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
    }
}

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

Организация взаимодействия потоков с помощью методов notify(), wait() и notifyAll()

Рассмотрим для примера следующую ситуацию. В потоке Т выполняется синхронизированный метод, которому необходим доступ к ресурсу R. Этот ресурс временно недоступен. Что должен предпринять поток т? Если он будет ожидать в цикле освобождения ресурса R, объект будет по-прежнему заблокирован и другие потоки не смогут обратиться к нему. Такое решение малопригодно, поскольку оно сводит на нет все преимущества программирования в многопоточной среде. Намного лучше, если поток Т временно разблокирует объект и позволит другим потокам воспользоваться его методами. Когда ресурс R станет доступным, поток т получит об этом уведомление и возобновит свое исполнение. Но для того чтобы такое решение можно было реализовать, необходимы средства взаимодействия потоков, с помощью которых один поток мог бы уведомить другой поток о том, что он приостановил свое исполнение, а также получить уведомление о том, что его исполнение может быть возобновлено. Для организации подобного взаимодействия потоков в Java предусмотрены методы wait(), notify() и notifyAll().

Эти методы реализованы в классе Object, поэтому они доступны для любого объекта. Но обратиться к ним можно только из синхронизированного контекста. А применяются они следующим образом. Когда поток временно приостанавливает свое исполнение, он вызывает метод wait(). При этом поток переходит в состояние ожидания и монитор данного объекта освобождается, позволяя другим потокам использовать объект. Впоследствии ожидающий поток возобновит свое выполнение, когда другой поток войдет в тот же самый монитор и вызовет метод notify() или notifyAll().

В классе Object определены различные формы объявления метода wait(), как показано ниже.

final void wait() throws InterruptedException
final void wait(long миллисекунд) throws InterruptedException
final void wait(long миллисекунд, int наносекунд) throws InterruptedException

В первой своей форме метод wait() переводит поток в режим ожидания до поступления уведомления. Во второй форме метода организуется ожидание уведомления или до тех пор, пока не истечет указанный период времени. А третья форма позволяет точнее задавать период времени в наносекундах.

Ниже приведены общие формы объявления методов notify() и notifyAll().

final void notifyO
final void notifyAll()

При вызове метода notify() возобновляется исполнение одного ожидающего потока. А метод notifyAll() уведомляет все потоки об освобождении объекта, и тот поток, который имеет наивысший приоритет, получает доступ к объекту.

Прежде чем рассматривать конкретный пример, демонстрирующий применение метода wait(), необходимо сделать важное замечание. Несмотря на то что метод wait() должен переводить поток в состояние ожидания до тех пор, пока не будет вызван метод notify() или notifyAll(), иногда поток выводится из состояния ожидания вследствие так называемой ложной активизации. Условия для ложной активизации сложны, возникают редко, а их обсуждение выходит за рамки этой книги. Но в компании Oracle рекомендуют учитывать вероятность проявления ложной активизации и помещать вызов метода wait() в цикл. В этом цикле должно проверяться условие, по которому поток переводится в состояние ожидания. Именно такой подход и применяется в рассматриваемом ниже примере.

Пример применения методов wait() и notify()

Для того чтобы стала понятнее потребность в применении методов wait() и notify() в многопоточном программировании, рассмотрим пример программы, имитирующей работу часов и выводящей на экран слова "Tick" (Тик) и "Тоск" (Так). Для этой цели создадим класс TickTock, который будет содержать два метода: tick() и tock(). Метод tick() выводит слово "Tick", а метод tock() — слово "Тоск". При запуске программы, имитирующей часы, создаются два потока: в одном из них вызывается метод tick(), а в другом — метод tock(). В результате взаимодействия двух потоков на экран будет выводиться набор повторяющихся сообщений "Tick Tock", т.е. после слова "Tick", обозначающего один такт, должно следовать слово "Тоск", обозначающее другой такт часов.

// Применение методов wait() и notifyO для имитации часов,
class TickTock {
    String state; // содержит сведения о состоянии часов
    synchronized void tick(boolean running) {
        if (!running) { // остановить часы
            state = "ticked";
            notifyO; // уведомить ожидающие потоки
            return;
        }

        System.out.print("Tick ");

        state = "ticked"; // установить текущее состояние после такта "тик"
        notify();         // Метод tick() уведомляет метод tock()
                          // о возможности продолжить выполнение.
        try {
            while(!state.equals("tocked") )
                wait();// Метод tick() ожидает завершения метода tock().
        }
        catch(InterruptedException exc) {
            System.out.println("Thread interrupted.");
        }
    }

    synchronized void tock(boolean running) {
        if(!running) { // остановить часы
            state = "tocked";
            notifyO; // уведомить ожидающие потоки
            return;
        }

        System.out.println("Tock");

        state = "tocked"; // установить текущее состояние после такта "так"
        notifyO; // Метод tock() уведомляет метод tick()
                 // возможности продолжить выполнение.
        try {
            while(!state.equals("ticked") )
                wait(); // Метод tock() ожидает завершения метода tick().
        }
        catch(InterruptedException exc) {
            System.out.println("Thread interrupted.");
        }
    }
}

class MyThread implements Runnable {
    Thread thrd;
    TickTock ttOb;

    // построить новый поток
    MyThread.(String name, TickTock tt) {
        thrd = new Thread(this, name);
        ttOb = tt;
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        if(thrd.getName().compareTo("Tick") == 0) {
            for(int i=0; i<5; i++) ttOb.tick(true);
                ttOb.tick(false);
        }
        else {
            for(int i=0; i<5; i++) ttOb.tock(true);
                ttOb.tock(false);
        }
    }
}

class ThreadCom {
    public static void main(String args[]) {
        TickTock tt = new TickTock();
        MyThread mtl = new MyThread("Tick", tt);
        MyThread mt2 = new MyThread("Tock", tt);

        try {
            mtl.thrd.join();
            mt2.thrd.join();
        } catch(InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
    }
}

В результате выполнения этой программы на экране появляются следующие сообщения:

Tick Tock
Tick Tock
Tick Tock
Tick Tock
Tick Tock

Рассмотрим более подробно исходный код программы, имитирующей работу часов. В ее основу положен класс TickTock. В нем содержатся два метода tick() и tock(), которые взаимодействуют друг с другом. Это взаимодействие организовано таким образом, чтобы за словом "Tick” всегда следовало слово "Tock", затем слово "Tick" и т.д. Обратите внимание на переменную state. В процессе работы имитатора часов в данной переменной хранится строка "ticked" или "tocked", определяющая текущее состояГлава 1 1. Многопоточное программирование 41.1 ние часов после такта “тик” или/‘так” соответственно. В методе main() создается объект tt типа TickTock, используемый для запуска двух потоков на исполнение.

Потоки строятся на основе объектов типа MyThread. Конструктору MyThread() передаются два параметра. Первый из них задает имя потока (в данном случае — "Tick" или "Тоск"), а второй — ссылку на объект типа TickTock (в данном случае — объект tt). В методе run() из класса MyThread вызывается метод tick(), если поток называется "Tick", или же метод tock(), если поток называется "Тоск". Каждый из этих методов вызывается пять раз с параметром, принимающим логическое значение true. Работа имитатора часов продолжается до тех пор, пока методу передается параметр с логическим значением true. Последний вызов каждого из методов с параметром, принимающим логическое значение false, останавливает имитатор работы часов.

Самая важная часть программы находится в теле методов tick() и tock() из класса TickTock. Начнем с метода tick(). Для удобства анализа ниже представлен исходный код этого метода.

synchronized void tick(boolean running) {
    if(!running) { // остановить часы
        state = "ticked";
        notifyO; // уведомить ожидающие потоки
        return;
    }

    System.out.print("Tick ");

    state = "ticked"; // установить текущее состояние после такта "тик"
    notify(); // уведомить метод tock() о возможности продолжить выполнение
    try {
        while(!state.equals("tocked") )
            wait(); // ожидать завершения метода tock()
    }
    catch(InterruptedException exc) {
        System.out.println("Thread interrupted.");
    }
}

Прежде всего обратите внимание на то, что в объявлении метода tick() присутствует ключевое слово synchronized, указываемое в качестве модификатора доступа. Как пояснялось ранее, действие методов wait() и notify() распространяется только на синхронизированные методы. В начале метода tick() проверяется значение параметра running. Этот параметр служит для корректного завершения программы, имитирующей работу часов. Если он принимает логическое значение false, имитатор работы часов должен быть остановлен. Если же параметр running принимает логическое значение true, а переменная state — значение "ticked", вызывается метод notify(), разрешающий ожидающему потоку возобновить свое исполнение. Мы еще вернемся к этому вопросу несколько ниже.

По ходу работы имитируемых часов в методе tick() выводится слово "Tick", переменная state принимает значение "ticked", а затем вызывается метод notify(). Вызов метода notify() возобновляет исполнение ожидающего потока. Далее в цикле while вызывается метод wait(). В итоге выполнение метода tick() будет приостановлено до тех пор, пока другой поток не вызовет метод notify(). Таким образом, очередной шаг цикла не будет выполнен до тех пор, пока другой поток не вызовет метод notify() для того же самого объекта. Поэтому когда вызывается метод tick(), на экран выводится слово "Tick" и другой поток получает возможность продолжить свое исполнение, а затем выполнение этого метода приостанавливается.

В том цикле while, в котором вызывается метод wait(), проверяется значение переменной state. Значение "tocked", означающее завершение цикла, будет установлено только после выполнения метода tock(). Этот цикл предотвращает продолжение исполнения потока в результате ложной активизации. Если по окончании ожидания в переменной state не будет присутствовать значение "tocked", значит, имела место ложная активизация, и метод wait() будет вызван снова.

Метод tock() является почти точной копией метода tick(). Его отличие состоит лишь в том, что он выводит на экран слово "Tock" и присваивает переменной state значение "tocked". Следовательно, когда метод tock() вызывается, он выводит на экран слово "Tock", вызывает метод notify(), а затем переходит в состояние ожидания. Если проанализировать работу сразу двух потоков, то станет ясно, что за вызовом метода tick() тотчас следует вызов метода tock(), после чего снова вызывается метод tick(), и т.д. В итоге оба метода синхронизируют друг друга.

При остановке имитатора работы часов вызывается метод not if у (). Это нужно для того, чтобы возобновить исполнение ждущего потока. Как упоминалось выше, в обоих методах, tick() и tock(), после вывода сообщения на экран вызывается метод wait(). В результате при остановке имитатора работы часов один из потоков обязательно будет находиться в состоянии ожидания. Следовательно, последний вызов метода notify() необходим. В качестве эксперимента попробуйте удалить вызов метода notify() и посмотрите, что при этом произойдет. Вы увидите, что программа зависнет, и вам придется завершить ее нажатием комбинации клавиш <Ctrl+C>. Дело в том, что когда метод tock() в последний раз получает управление, он вызывает метод wait(), после чего не происходит вызов метода not if у (), позволяющего завершиться методу tock(). В итоге метод tock() остается в состоянии бесконечного ожидания.

Если у вас еще остаются сомнения по поводу того, что методы wait() и notify() необходимы для организации нормального выполнения программы, имитирующей работу часов, замените в ее исходном коде класс TickTock приведенным ниже его вариантом. Он отличается тем, что в нем удалены вызовы методов wait() и notify().

// В этой версии вызовы методов wait() и notify() отсутствуют,
class TickTock {

    String state; // содержит сведения о состоянии часов

    synchronized void tick(boolean running) {
        if(!running) { // остановить часы
            state = "ticked";
            return;
        }

        System.out.print("Tick ");

        state = "ticked"; // установить текущее состояние после такта "тик"
    }

    synchronized void tock(boolean running) {
        if(!running) { // остановить часы
            state = "tocked";
            return;
        }

        System.out.println("Tock") ;

        state = "tocked"; // установить текущее состояние после такта "так"
    }
}

Теперь программа выводит на экран следующие сообщения:

Tick Tick Tick Tick Tick Tock
Tock
Tock
Tock
Tock

Это происходит потому, что методы tick() и tock() не взаимодействуют друг с другом.

Приостановка, возобновление и остановка потоков

Иногда оказывается полезно приостановить или даже полностью прекратить исполнение потока. Допустим, отдельный поток используется для отображения времени. Если пользователю не нужны часы на экране, то отображающий их поток можно приостановить. Независимо от причин, по которым требуется временная остановка потока, сделать это нетрудно, как, впрочем, и возобновить исполнение потока.

Механизмы приостановки, возобновление и остановки потоков менялись в разных версиях Java. До появления версии Java 2 для этих целей применялись методы suspend(), resume() и stop(), определенные в классе Thread. Ниже приведены общие формы их объявления.

final void resume()
final void suspend()
final void stop()

На первый взгляд кажется, что упомянутые выше методы удобны для управления потоками, но пользоваться ими все же не рекомендуется по следующим причинам. При выполнении метода suspend() иногда возникают серьезные осложнения, приводящие к взаимоблокировке. Метод resume() сам по себе безопасен, но применяется только в сочетании с методом suspend(). Что же касается метода stop() из класса Thread, то и он не рекомендуется к применению, начиная с версии Java 2, поскольку может вызывать порой серьезные осложнения в работе многопоточных программ.

Если методы suspend(), resume() и stop() нельзя использовать для управления потоками, то может показаться, что приостановить, возобновить и остановить поток вообще нельзя. Но это, к счастью, не так. Поток следует разрабатывать таким образом, чтобы в методе run() периодически осуществлялась проверка, следует ли приостановить, возобновить или остановить поток. Обычно для этой цели используются две флаговые переменные: одна — для приостановки и возобновления потока, другая — для остановки потока. Если флаговая переменная, управляющая приостановкой потока, установлена в состояние исполнения, то метод run() должен обеспечить продолжение исполнения потока. Если же эта флаговая переменная находится в состоянии приостановки, в работе потока должна произойти пауза. А если переменная, управляющая остановкой потока, находится в состоянии остановки, исполнение потока должно прекратиться.

Следующий пример программы демонстрирует один из способов реализации собственных версий методов suspend(), resume() и stop().

// Приостановка, возобновление и остановка потока.
class MyThread implements Runnable {
    Thread thrd;

    // Если эта переменная принимает логическое значение
    // true, исполнение потока приостанавливается.
    volatile boolean suspended;
    // Если эта переменная принимает логическое значение
    // true, исполнение потока прекращается.
    volatile boolean stopped;

    MyThread(String name) {
        thrd = new Thread(this, name);
        suspended = false;
        stopped = false;
        thrd.start();
    }

    // Точка входа в поток
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        try {
            for(int i = 1; i < 1000; i++) {
                System.out.print(i + " ");
                if((i %10)==0) {
                    System.out.println() ;
                    Thread.sleep(250) ;
                }

                // Для проверки условий приостановки и остановки потока
                // используется следужхций синхронизированный блок.
                synchronized(this) {
                    while(suspended) {
                        wait();
                    }
                    if(stopped) break;
                }
            }
        } catch (InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " exiting.");
    }

    // остановить поток
    synchronized void mystopO {
        stopped = true;

        // Следующие операторы обеспечивают полную
        // остановку приостановленного потока,
        suspended = false;
        notify();
    }

    // приостановить поток
    synchronized void mysuspend() {
        suspended = true;
    }

    // возобновить поток
    synchronized void myresume() {
        suspended = false;
        notify();
    }
}

class Suspend {
    public static void main(String args[]) {
        MyThread obi = new MyThread("My Thread");

        try {
            Thread.sleep(1000); // позволить потоку оЫ начать исполнение

            obi.mysuspend();
            System.out.println("Suspending thread.");
            Thread.sleep(1000);

            obi.myresume();
            System.out.println("Resuming thread.");
            Thread.sleep(1000);

            obi.mysuspend();
            System.out.println("Suspending thread.");
            Thread.sleep(1000);

            obi.myresume();
            System.out.println("Resuming thread.") ;
            Thread.sleep(1000);

            obi.mysuspend() ;
            System.out.println("Stopping thread.");
            obi.mystop();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        // ожидать завершения потока
        try {
            obi.thrd.join() ;
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

Ниже приведен результат выполнения данной программы.

My Thread starting.
123456789 10
11 12   13  14  15  16  17  18  19  20
21 22   23  24  25  26  27  28  29  30
31 32   33  34  35  36  37  38  39  40
Suspending thread.
Resuming thread.
41 42   43  44  45  46  47  48  49  50
51 52   53  54  55  56  57  58  59  60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
Suspending thread.
Resuming thread.
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99 100
101 102 103 104 105 106 107 108 109 110
111 112 113 114 115 116 117 118 119 120
Stopping thread.
My Thread exiting.
Main thread exiting.

Эта программа работает следующим образом. В классе потока MyThread определены две логические переменные, suspended и stopped, управляющие временной и полной остановкой потока. В конструкторе этого класса обеим переменным присваивается логическое значение false. Метод run() содержит синхронизированный блок, в котором проверяется состояние переменной suspended. Если эта переменная принимаетлогическое значение true, вызывается метод wait(), приостанавливающий исполнение потока. Логическое значение true присваивается переменной suspended в методе mysuspend(), и поэтому данный метод следует вызвать для приостановки потока. Для возобновления потока служит метод myresume(), в котором переменной suspended присваивается логическое значение false и вызывается метод not if у ().

Для остановки потока следует вызвать метод my stop(), в котором переменной stopped присваивается логическое значение true. Кроме того, в методе mystop() переменной suspended присваивается логическое значение false и вызывается метод notify(). Это необходимо для прекращения работы потока, исполнение которого ранее было приостановлено.

В отношении рассматриваемой здесь программы нужно сделать еще одно, последнее замечание. В объявлении переменных suspended и stopped используется ключевое слово volatile. Этот модификатор подробно описывается в главе 14, а до тех пор вкратце поясним его назначение. Он сообщает компилятору о том, что значение переменной может быть неожиданно изменено другими частями программы, в том числе и другим потоком.

Пример для опробования 11.2. Применение основного потока

В каждой программе на Java присутствует хотя бы один поток, называемый основным. Этот поток получает управление автоматически при запуске программы на выполнение. В этом проекте будет продемонстрировано, что основным потоком можно управлять таким образом же, как и любым другим.

Последовательность действий

  1. Создайте файл UseMain.java.
  2. Для доступа к основному потоку нужно получить ссылающийся на него объект типа Thread. Для этого следует вызвать метод currentThread(), являющийся статическим членом класса Thread. Ниже приведено объявление этого метода.
    static Thread currentThread()
    
    Метод currentThread() возвращает ссылку на тот поток, из которого он вызывается. Так, если вызвать метод currentThread() из основного потока, можно получить ссылку на этот поток. А имея ссылку на основной поток, можно управлять им.
  3. Введите в файл UseMain. j ava приведенный ниже исходный код программы. В процессе ее выполнения сначала извлекается ссылка на основной поток, затем определяется и устанавливается имя и приоритет потока.
    /*
    Пример для опробования 11.2.
    
    Управление основным потоком.
    */
    class UseMain {
        public static void main(String args[]) {
            Thread thrd;
    
            // получить основной поток
            thrd = Thread.currentThread();
    
            // отобразить имя основного потока
            System.out.println("Main thread is called: " +
                               thrd.getName());
    
            // отобразить приоритет основного потока
            System.out.println("Priority: " +
                               thrd.getPriority());
    
            System.out.println();
    
            // установить имя и приоритет основного потока
            System.out.println("Setting name and priority.\n");
            thrd.setName("Thread #1");
            thrd.setPriority(Thread.NORM_PRI0RITY+3);
    
            System.out.println("Main thread is now called: " +
                               thrd.getName());
    
            System.out.println("Priority is now: " +
                               thrd.getPriority());
        }
    }
    
  4. Ниже приведен результат выполнения данной программы.
    Main thread is called: main
    Priority: 5
    
    Setting name and priority.
    
    Main thread is now called: Thread #1
    Priority is now: 8
    
  5. Выполняя операции над основным потоком, необходимо соблюдать осторожность. Так, если добавить в конце метода main() приведенный ниже код, программа никогда не завершится, потому что будет ожидать завершения основного потока!
    try {
        thrd.join();
    } catch(InterruptedException exc) {
        System.out.println("Interrupted");
    }
    

Упражнение для самопроверки по материалу главы 11

  1. Каким образом имеющиеся в Java средства многопоточного программирования позволяют писать более эффективные программы?
  2. Для поддержки многопоточного программирования в Java предусмотрен класс и интерфейс .
  3. В каких случаях следует отдать предпочтение расширению класса Thread над реализацией интерфейса Runnable?
  4. Покажите, как с помощью метода j oin() можно организовать ожидание завершения потокового объекта MyThrd.
  5. Покажите, как установить приоритет потока MyThrd на три уровня выше нормального приоритета.
  6. Что произойдет, если в объявлении метода указать ключевое слово synchronized?
  7. Методы wait() и notify() служат для ____________ .
  8. Внесите в класс TickTock изменения для организации настоящего отчета времени. Первую половину секунды должен занимать вывод на экран слова "Tick", а вторую — вывод слова "Tock". Таким образом, сообщение "Tick-Tock" должно соответствовать одной секунде отсчитываемого времени. (Время переключения контекстов можно не учитывать.)
  9. Почему в новых программах на Java не следует применять методы suspend(), resume() и stop()?
  10. С помощью какого метода из класса Thread можно получить имя потока?
  11. Какое значение возвращает метод isAlive() ?
  12. Попытайтесь самостоятельно реализовать средства синхронизации в классе Queue, разработанном в предыдущих главах. В результате доработки класс должен действовать правильно, когда он используется для многопоточной обработки.