Главная » Java » Создание потоков

0

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

Thread worker = new Thread();

После того как объект-поток будет создан, вы можете задать его конфигурацию и запустить. В понятие конфигурации потока входит указание исходного приоритета, имени и так далее. Когда поток готов к работе, следует вызвать его метод start. Метод start порождает новый выполняемый поток на основе данных объекта класса Thread, после

чего завершается. Метод start вызывает метод run нового потока, что приводит к

активизации последнего.

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

Стандартная реализация Thread.run не делает ничего. Вы должны либо расширить класс Thread, чтобы включить в него новый метод run, либо создать объект Runnable и передать его конструктору потока. Сначала мы рассмотрим процесс порождения новых потоков за счет расширения Thread, а позже займемся техникой работы с Runnable (см. “Использование Runnable”).

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

“ping” и “PONG” с различной частотой:

class PingPong extends Thread {

String word;               // выводимое слово

int delay;                 // длительность паузы

PingPong(String whatToSay, int delayTime) {

word = whatToSay;

delay = delayTime;

}

public void run() {

try {

for (;;) {

System.out.print(word + " ");

sleep(delay);   // подождать следующего вывода

}

} catch (InterruptedException e) {

return;

}

}

public static void main(String[] args) {

new PingPong("ping",  33).start(); // 1/30 секунды

new PingPong("PONG", 100).start(); // 1/10 секунды

}

}

Мы определили тип потока с именем PingPong. Его метод run работает в бесконечном цикле, выводя содержимое поля word и делая паузу на delay микросекунд. Метод PingPong.run не может возбуждать исключений, поскольку этого не делает переопределяемый  им метод Thread.run. Соответственно, мы должны перехватить исключение InterruptedException, которое может возбуждаться методом sleep.

После этого можно непосредственно создать выполняющиеся потоки — именно это и делает метод PingPong. Он конструирует два объекта PingPong, каждый из которых обладает своим выводимым словом и интервалом задержки, после чего вызывает методы start обоих объектов-потоков.  С этого момента и начинается работа потоков. Примерный результат работы может выглядеть следующим образом:

ping PONG ping ping PONG ping ping ping PONG ping ping PONG ping ping ping PONG ping ping PONG ping ping ping PONG ping ping ping PONG ping ping PONG ping ping ping PONG ping ping ping PONG ping ping PONG ping ping ping PONG ping ping ping PONG ping

ping ping PONG ping ping PONG ping ping ping PONG …

Поток может обладать именем, которое передается в виде параметра типа String либо конструктору, либо методу setName. Вы получите текущее имя потока, если вызовете метод getName. Имена потоков предусмотрены исключительно для удобства программиста — в системе runtime в Java они не используются.

Вызов статического метода Thread.currentThread позволяет получить объект Thread,

который соответствует работающему в настоящий момент потоку.

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

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

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

9.2.1. Методы synchronized

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

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

Синхронизация решает проблему, возникающую в нашем примере: если действия выполняются в синхронизированном методе, то при попытке обращения к объекту со стороны второго потока в тот момент, когда с объектом работает первый поток, доступ будет отложен до снятия блокировки.

Приведем пример того, как мог бы выглядеть класс Account, спроектированный  для работы в многопоточной среде:

class Account {

private double balance;

public Account(double initialDeposit) {

balance = initialDeposit;

}

public synchronized double getBalance() {

return balance;

}

public synchronized void deposit(double amount) {

balance += amount;

}

}

А теперь мы объясним, что же означает понятие “соответствующие”  применительно к синхронизированным методам.

Конструктор не обязан быть synchronized, поскольку он выполняется только при создании объекта, а это может происходить только в одном потоке для каждого вновь создаваемого объекта. Поле balance защищено от любых несинхронных изменений за счет использования методов доступа, объявленных synchronized. В этом заключается еще одна причина, по которой вместо объявления полей public или protected следует применять методы для работы с ними: так вы сможете контролировать синхронизацию доступа к

ним.

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

результате могло бы получиться частично искаженное значение. Объявление synchronized

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

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

Если синхронизированный метод переопределяется  в расширенном классе, то новый метод не обязан быть синхронизированным. Метод суперкласса при этом остается синхронизированным, так что несинхронность метода в расширенном классе не отменяет его синхронизированного поведения в суперклассе. Если в несинхронизированном методе используется конструкция super.method() для обращения к методу суперкласса, то объект блокируется на время вызова до выхода из метода суперкласса.

9.2.2. Операторы synchronized

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

synchronized (выражение)

оператор

Взятое в скобки выражение должно указывать на блокируемый объект — обычно оно является ссылкой на объект. После блокировки выполняется оператор — так, словно для данного объекта выполняется синхронизированный метод. Чаще всего при блокировке объекта необходимо выполнить сразу несколько операторов, так что оператор, как правило, представляет собой блок. Приведенный ниже метод заменяет каждый элемент числового массива его модулем, причем доступ к массиву регулируется оператором synchronized:

/** сделать все элементы массива неотрицательными */

public static void abs(int[] values) {

synchronized (values) {

for (int i = 0; i << values.length; i++) {

if (values[i] << 0)

values[i] = – values[i];

}

}

}

Массив values содержит изменяемые элементы. Мы синхронизируем доступ к нему, указывая его в качестве объекта в операторе synchronized. После этого можно выполнять цикл и быть уверенным в том, что массив не будет изменен каким-либо другим фрагментом программы, в котором тоже установлена синхронизация для массива values.

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

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

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

Создать расширенный класс, в котором вы переопределяете нужные методы, объявляете их synchronized и перенаправляете вызовы этих методов при помощи ссылки super.

Воспользоваться оператором synchronized для обеспечения доступа к объекту, с которым могут работать несколько потоков.

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

Источник: Арнольд К., Гослинг Д. – Язык программирования Java (1997)

По теме:

  • Комментарии