Потоки в Java (java threads)

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

Если сказать просто, то поток(thread) – это путь программного выполнения. Большинство программ, написанных сегодня, запускаются одним потоком, проблемы начинают возникать, когда несколько событий или действий должны произойти в одно время. Допустим, например, программа не способна рисовать картинку пока выполняет чтение нажатия клавиш. Программа должна уделять всё своё внимание клавиатуре, вследствие чего отсутствует возможность обрабатывать более одного события одновременно. Идеальным решением для этой проблемы может служить возможность выполнения двух или более разделов программы в одно время. Потоки позволяют нам это сделать.

Многопоточные приложения предоставляют мощь при запуске многих потоков в рамках одной программы. С логической точки зрения, многопоточность означает, что несколько строк из одной и той же программы могут быть выполнены в одно и то же время, однако, это не то же самое, что запустить программу дважды и сказать, что несколько строк кода выполняются в одно время. В этом случае, операционная система обрабатывает две программы раздельно и как отдельные процессы. В Unix, разветвляющий(forking) процесс создаёт дочерний процесс с разным адресным пространством для кода и данных. Вместе с тем, fork() создаёт много накладок для операционной системы, это влечёт за собой интенсивную нагрузку на процессор. При запуске потока, эффективный путь выполнения создаётся за счёт распределения исходного пространство данных родителя. Идея совместного использования данных очень выгодна, но вызывает некоторые вопросы, которые мы обсудим позже.


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


Создатели Java милостиво предоставили две возможности создания потоков: реализовать(implementing) интерфейс и расширить(extending) класс. Расширение класса это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса. Это ограничение внутри Java можно побороть реализацией интерфейса, который является наиболее распространённым способом создания потоков. (Заметим, что способ наследования позволяет только запустить класс как поток. Это позволяет классу только выполнить start() и т.п.).

Интерфейсы дают возможность программистам заложить фундамент одного класса. Они используются для разработки требований к набору классов для реализации. Интерфейс устанавливает правила. Разные наборы классов, которые реализуют интерфейс, должны следовать этим правилам.

Есть некоторые отличия между классом и интерфейсом. Во-первых, интерфейс может только содержать абстрактные методы и/или static final переменные (константы). Классы, с другой стороны, могут реализовывать методы и содержать переменные, которые не выступают в качестве констант. Во-вторых, интерфейс не может реализовывать никаких методов. Класс, который реализовывает интерфейс, должен реализовать все методы, которые описаны в интерфейсе. У интерфейса есть возможность расширяться за счёт других интерфейсов, и (в отличие от классов) могут расширяться от нескольких интерфейсов. К тому же, экземпляр интерфейса не может быть создан, используя, оператор new; например, Runnable a = new Runnable(); не разрешается.

Для первого способа создания потока необходимо просто наследоваться от класса Thread. Делайте так, только если классу нужно только выполниться как отдельному потоку и никогда не понадобиться наследоваться от другого класса. Класс Thread определён в пакете java.lang, который необходимо импортировать, что бы наши классы знали о его описании:
 
import java.lang.*;
public class Counter extends Thread {
    public void run() {
        ....
    }
}
 
Пример выше создаёт новый класс Counter, который расширяет класс Thread и подменяет метод Thread.run() для своей реализации. В методе run() происходит вся работа класса Counter как потока. Такой же класс можно создать, реализуя интерфейс Runnable:
 
import java.lang.*;
public class Counter implements Runnable {
    Thread T;
    public void run() {
  ....
    }
}
 
Здесь осуществляется абстрактный метод run(), который описан в интерфейсе Runnable. Отметим, что у нас есть экземпляр класса Thread, переменная класса Counter. Единственное отличие этих двух методов заключается в том, что реализация Runnable является более гибкой для создания класса Counter. В примере, который описан выше, есть возможность расширения класса Counter, если в этом есть такая необходимость. Большинство классов, которые должны выполняться как потоки, реализуют Runnable, поскольку они, вероятно, могут расширить свою функциональность за счёт другого класса.

Не подумайте, что интерфейс Runnable выполняет какую-то реальную работу, когда поток запускается. Это всего лишь класс, созданный для того, что бы дать представление о классе Thread. На самом деле, это очень небольшой, содержащий только один абстрактный метод интерфейс. Это описание интерфейса Runnable, взятое из исходных кодов Java:
 
package java.lang;
public interface Runnable {
    public abstract void run();
}
 
Это и всё, что есть в интерфейсе Runnable. Интерфейс – это всего лишь описание, которое классы должны реализовать. Итак, Runnable, заставляет только запустить метод run(). Вследствие этого, большая часть работы полагается на класс Thread. Более пристальный взгляд на класс Thread даст представление о том, что на самом деле происходит:
public class Thread implements Runnable {
    ...
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    ...
}
 
Во фрагменте кода, который представлен выше видно, что класс Thread, так же реализует интерфейс Runnable. Thread.run() выполняет проверку, что бы удостовериться в том, что этот класс (класс, который выполняется как поток) не равен null, и только потом выполняет метод run(). Когда это произойдёт, то метод run() запустит поток.

Запуск и остановка


Различные способы создания объектов потока сейчас очевидны, мы продолжим дискуссию о реализации потоков, начиная с путей запуска и остановки их, используя, маленький applet, который содержит поток для иллюстрации этих механизмов:
Апплет выше, начнёт отсчёт с нуля и будет отображать это и на экране, и в консоль. На первый взгляд может сложиться впечатление, что программа начинает отсчёт и показывает каждую цифру, но это не так. При более внимательном рассмотрении этого апплета будет выявлено, как на самом деле работает этот апплет.

В этом случае, класс CounterThread был вынужден реализовать интерфейс Runnnable, что бы дальше была возможность расширить класс Applet. Все апплеты начинают свою работу с метода init(), переменная Cout инициализируется нулём и создаётся новый объект класса Thread. Передавая this в конструктор класса Thread, таким образом, новый поток будет знать какой объект запускается. В этом случает this это ссылка на CounterThread. После того как поток создан его нужно запустить. Вызываем метод start(), который в свою очередь вызывает метод run() объекта CounterThread, то есть CounterThread.run(). Сразу выполниться метод start() и в это же время начнёт свою работу поток. Заметим, что в методе run() бесконечный цикл. Он бесконечен, потому что, как только выполниться метод run(), то поток закончит работу. Метод run() будет инкрементировать переменную Count, ожидать(sleep) 10 секунд и посылать запрос на обновление экрана апплета.

Заметим, что вызова метода sleep именно в потоке является очень важным. Если это не так, то программа займёт всё процессорное время для своего процесса и не даст возможности любым другим методам, например методам, выполниться. Другой способ остановить выполнение потока это вызвать метод stop(). В данном примере, поток останавливается, когда происходит нажатие мыши в пределах апплета. В зависимости от скорости компьютера, на котором запущен апплет, не все числа будут отображены, потому что инкрементирование происходит независимо от прорисовки апплета. Апплет может не обновляться после каждого запроса на прорисовку, так как ОС может поставить запрос в очередь запросов и последующие запросы на обновление будут удовлетворены с одним запросом. Пока запросы на перерисовку собираются в очередь, переменная Count продолжает увеличиваться, но не отображается.
Приостановка и возобновление

Когда поток остановлен с использованием метода stop() он уже не может быть возобновлён с использованием метода start(), сразу после вызова метода stop() происходит уничтожение выполняющегося потока. Вместо этого вы можете приостановить выполнение потока, используя метод sleep() на определённый отрезок времени и потом выполнение потока продолжится, когда выйдет время. Но это не самое лучшее решение, если поток необходимо запустить, когда произойдёт определённое условие. Для этого, используется метод suspend(), который даёт возможность временно прекратить выполнение потока и метод resume(), который позволяет продолжить выполнение потока. Следующий апплет является изменением апплета, который был дан выше, но с использованием методов suspend() и resume():
public class CounterThread2 extends Applet implements Runnable { 
    Thread t;
    int Count; 
    boolean suspended; 
    public boolean mouseDown(Event e,int x, int y) { 
        if(suspended) 
            t.resume(); 
        else 
            t.suspend(); 
        suspended = !suspended; 
        return true; 
    } 
    ... 
}
 

Для того чтобы сохранить текущее состояние апплета используется логическая(boolean) переменная suspended. Характеристики разных состояний апплета является важной частью, потому что некоторые методы могут выкидывать исключения, если они вызываются не из того состояния. Например, если поток запущен и остановлен, вызов метода start() приведёт к исключению IllegalThreadStateException.


Планирование


В Java есть Планировщик Потоков(Thread Scheduler), который контролирует все запущенные потоки во всех программах и решает, какие потоки должны быть запущены, и какая строка кода выполняться. Существует две характеристики потока, по которым планировщик идентифицирует процесс. Первая, более важная, это приоритет потока, другая, является-ли поток демоном(daemon flag). Простейшее правило планировщика, это если запущены только daemon потоки, то Java Virtual Machine (JVM) вызгрузиться. Новые потоки наследуют приоритет и daemon flag от потока, который его создал. Планировщик определяет какой поток должен быть запущен, анализируя приоритет всех потоков. Потоку с наивысшим приоритетом позволяется выполниться раньше, нежели потокам с более низкими приоритетами.

Планировщик может быть двух видов: с преимуществом и без. Планировщик с преимуществом предоставляет определённый отрезок времени для всех потоков, которые запущены в системе. Планировщик решает, какой поток следующий запуститься или возобновить работу через некоторый постоянный период времени. Когда поток запуститься, через этот определённый промежуток времени, то выполняющийся поток будет приостановлен и следующий поток возобновит свою работу. Планировщик без приоритета решает, какой поток должен запуститься и выполняться до того, пока не закончит свою работу. Поток имеет полный контроль над системой настолько долго, сколько ему захочется. Метод yield() можно использовать для того чтобы принудить планировщик выполнить другой поток, который ожидает своей очереди. В зависимости от системы, на которой запущена Java, планировщик может быть либо с преимуществом, либо без него.


Приоритеты


Планировщик определяет, какой поток должен запуститься, основываясь на номер приоритета, назначенный каждому потоку. Приоритет потока может принимать значения от 1 до 10. По умолчанию, значение приоритета для потока является Thread.NORM_PRIORITY, которому соответствует значение 5. Так же доступны две других static переменных: Thread.MIN_PRIORITY, значение 1, и Thread.MAX_PRIORITY – 10. Метод getPriority() может использоваться для получения текущего значения приоритета соответствующего потока.
 

Daemon потоки


Такие потоки иногда ещё называются “службами”, которые обычно запускаются с наименьшим приоритетом и обеспечивают основные услуги для программы или программ, когда деятельность компьютера понижается. Примером такого потока может служить сборщик мусора. Этот поток, предусмотрен JVM, сканирует программы на наличие переменных, к которым больше никогда не придется обращаться, и освобождает их ресурсы, возвращая их системе. Поток может стать daemon потоком, передав булево значение true в метод setDaemon(). Если принято значение false, то поток становится обычным пользовательским потоком. Тем не менее, это необходимо сделать до того как поток запустится.
 

Пример планирования


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


Заключение


Использование потоков в Java позволит программистам более гибко использовать преимущества Java в их программах. Простота создания, настраивания и запуска потоков даст Java программистам возможность разрабатывать переносимые и мощные апплеты/приложения, которые невозможно выполнить в других языках третьего поколения. Потоки позволяют любым программам выполнять несколько задач как одну. В интернет ориентированных(Internet-aware) языках, таких как Java, это очень важный инструмент.
-----------
Перевел: sleepwalker
Оригинал статьи: http://www.javaworld.com/javaworld/jw-04-1996/jw-04-threads.html

Администрирование

Сегодня
Вчера
Эта неделя
Прошлая неделя
Этот месяц
Прошлый месяц
Вся статистика
77
3
77
26686
132
219
26793

IP: 3.14.254.204
Время: 2024-09-16 18:50:57
Счетчик joomla