1078
Часть
11.
Библиотека
Java
Барьер х у z Барьер
достигнут!
достигнут!
Как показывает рассмотренный выше пример, класс CyclicBarrier
ставляет изящное решение задачи, которая раньше считалась сложной.
предо
Класс
Exchanger
Вероятно,
наиболее
интересным
с
точки
зрения
синхронизации
является
класс
Exchanger,
предназначенный
для
упрощения
процесса
обмена
данными
между
двумя
потоками
исполнения.
Принцип
действия
класса
Exchanger
очень
прост:
он
ожидает
до
тех
пор,
пока
два
отдельных
потока
исполнения
не
вызовут
его
ме
тод
exchange
().Как
только
это
произойдет,
он
произведет
обмен
данными,
пре
доставляемыми обоими
потоками.
Такой
механизм
обмена
данными
не
только
из
ящен,
но
и
прост
в
применении.
Нетрудно
представить,
как
воспользоваться
клас
сом
Exchanger.
Например,
один
поток
исполнения
подготавливает
буфер
для
приема
данных
через
сетевое
соединение,
а
другой
-
заполняет
этот
буфер
данны
ми,
поступающими
через
сетевое
соединение.
Оба
потока исполнения
действуют
совместно,
поэтому
всякий
раз,
когда
требуется
новая
буферизация, осуществля
ется обмен данными. |
|
|
|
|
Класс Exchanger |
является |
обобщенным и |
объявляется |
приведенным |
образом, где параметр V определяет тип обмениваемых данных. |
||||
ниже
Exchanger<V>
В классе Exchanger определяется
щий следующие общие формы:
единственный
метод
exchange
(),имею
V V
exchange(V |
буфер) |
throws InterruptedException |
exchange(V |
буфер, |
long ожидание, TimeUnit единица_времени) |
throws InterruptedException, TimeoutException |
||
Здесь
параметр
буфер
обозначает ссылку
на
обмениваемые
данные.
Возвращаются
данные,
полученные
из
другого
потока
исполнения.
Вторая
форма
метода exchange
()
позволяет
определить
время
ожидания.
Главная
особенность
метода
exchange
( )
состоит
в
том,
что
он
не
завершится
успешно
до
тех
пор,
пока
не
будет
вызван
для одного
и
того
же
объекта
типа
Exchanger
из
двух
отдельных
потоков
исполнения.
Подобным
образом
метод
exchange
( )
синхронизирует
об
мен
данными.
В
приведенном
ниже
примере
программы
демонстрируется
применение
класса
Exchanger.
В
этой
программе
создаются
два
потока
исполнения.
В
одном
потоке
исполнения
создается
пустой
буфер,
принимающий
данные
из
другого
потока
ис
полнения.
Таким
образом,
первый
поток
исполнения
обменивает
пустую
символь
ную
строку
на полную.
Глава
28.
Служебные
средства параллелизма
1101
поддерживаются последовательности определяемых пользователем
А в суммирующих классах накапливается нарастающая сумма.
операций.
Параллельное программирование
Fork/Join Framework
средствами
В
последние
годы
появилось
новое
важное
направление
в
разработке
програм
много
обеспечения,
называемое
параллельным
программированием.
Параллельное
программирование
-
это
общее
название
методик,
выгодно
использующих
вы
числительные мощности
многоядерных
процессоров.
Как известно,
ныне
компью
теры
с
многоядерными
процессорами
уже
стали
обычным
явлением.
К
преимуще
ствам
многопроцессорных
систем
относится
возможность
значительно
повысить
производительность
программного
обеспечения.
В
итоге
заметно
возросла
по
требность
в
механизме,
который
позволял
бы
программирующим
на
Java
просто,
но
эффективно
пользоваться
несколькими
процессорами,
без
особого
труда
нара
щивая
вычислительные
мощности
по
мере
надобности.
В
ответ
на
эту
потребность
в
версии
JDK
7
было
внедрено
несколько
новых
классов
и
интерфейсов
для
под
держки
параллельного
программирования.
Обычно
они
упоминаются
под
общим
названием
Fork/Joiп
Framework.
Это
одно
из
наиболее
важных
за
последнее
время
дополнений
библиотеки
классов
Java.
Каркас
Fork!Join Framework
определен в
па
кете j ava. util. concurrent.
Каркас Fork/Join Framework усовершенствует
многопоточное
программирова
ние двумя
важными
способами.
Во-первых,
он
упрощает
создание
и
использование
нескольких
потоков
исполнения
и,
во-вторых,
автоматизирует
использование
не
скольких
процессоров.
Иными
словами,
каркас
Fork!Join
Framework
позволяет
авто
матически
наращивать
вычислительные
мощности
в
прикладных
программах,
уве
личивая
число
задействованных
процессоров.
Благодаря
этим
двум
усовершенство
ваниям
каркас
Fork/Join
Framework
рекомендуется
применять
для
многопоточноrо
программирования
в
тех
случаях,
когда
требуется
параллельная
обработка.
Прежде
чем
продолжить
дальше,
следует
указать
на
отличие
параллельного
программирования
от
традиционного
многопоточного
программирования.
В
про
шлом
большинство
компьютеров
имело
лишь
один
процессор,
и
многопоточность
позволяла
выгодно
воспользоваться
временем
простоя,
когда
программа
ожидает,
например,
ввода
данных
от
пользователя.
При
таком
подходе
один
поток
может
выполняться,
в
то
время
как
другой
ожидает.
Иными
словами,
в
системе
с
одним
процессором
многопоточность
позволяет
совместно
использовать
этот
процессор
для
выполнения
двух
или
более
задач.
Такой
тип
многопоточности,
как
правило,
поддерживается
объектом
класса
Thread,
как
пояснялось
в
главе
11.
И
хотя
эта
разновидность
многопоточности
останется
весьма
полезной
и
впредь,
она
не
со
всем
подходит
для
тех
случаев,
когда
имеются
два
или
более
процессора,
т.е.
мно
гоядерный
компьютер.
Если
имеется
несколько
процессоров,
то
требуется
другой
тип
многопоточ
ности,
обеспечивающий
настоящее
параллельное
выполнение.
На
нескольких
Глава
28.
Служебные
средства
параллелизма
1117
в
Имеются |
и другие способы асинхронного |
выполнения подзадач. Например, |
следующем |
фрагменте кода метод fork () |
вызывается для запуска подзадачи |
subTaskA,
а
метод
invoke
()
-
для
запуска
и ожидания
завершения
подзада
чи
subTaskB:
subTaskA. fork (); sum = subTaskA.
join()
+
subTaskB.invoke();
В качестве еще одного |
варианта можно непосредственно |
compute () для подзадачи |
subTaskB, как показано ниже. |
subTaskA. fork (); |
|
sum = subTaskA.join() + |
subTaskB.compute(); |
вызвать
метод
Асинхронное
выполнение
задач
Для
инициализации
задачи
в
приведенных
ранее
примерах
программ
вызвался
метод
invoke
()
из
класса
ForkJoinPool.
Это
общепринятый
подход,
когда
вы
зывающий
поток
исполнения
должен
ожидать
завершения
задачи
(что
зачастую
и бывает),
поскольку
метод
invoke
( )
не
завершится
до
тех
пор,
пока
не
завер
шится
задача.
Но
задачу
можно
запустить
на
выполнение
асинхронно.
При
таком
подходе
вызывающий
поток
продолжает
выполняться.
Таким
образом,
вызываю
щий
поток
и
задача
выполняются
одновременно.
Чтобы
запустить
задачу
на
вы
полнение
асинхронно,
следует вызвать
метод
execute
(),который
также
опреде
ляется
в
классе
ForkJoinPool.
Ниже
приведены
две
его
общие
формы.
void void
execute(ForkJoinTask<?> execute(RunnaЫe задача)
задача)
В
обеих
формах
этого
метода
задается
выполняемая
задача.
Обратите
вни
мание
на
то,
что
вторая
форма
позволяет
определить
задачу
типа
RunnaЬle,
а
не
F
о
r
kJ
о
i
n
Та
s k.
Этим
наводится
своего
рода
мост
между
традиционным
подходом
к
многопоточности
в
Java
и
новым
каркасом
Fork/Join Framework.
Следует,
однако,
иметь
в
виду,
что
потоки
исполнения,
используемые
пулом
типа
ForkJoinPool,
являются
потоковыми демонами.
Следовательно,
они
завершаются
по
окончании
основного
потока
исполнения.
Это
означает,
что основной
поток
исполнения,
воз
можно,
придется
поддерживать
в
активном
состоянии
до
тех
пор,
пока
не
завер
шатся
все
задачи.
Отмена
задачи
Вызвав метод cancel (), определенный в классе ForkJoinTask, менить задачу. Ниже приведена общая форма этого метода.
можно
от
boolean
cancel(boolean
прерывание)
он
Этот |
метод |
возвращает логическое значение true, если |
задача, |
для которой |
был |
вызван, |
успешно отменена, или логическое значение |
false, |
если задача |
уже
отменена,
завершена
или
не
может
быть
отменена.
В
настоящее
время
пара
метр
прерывание
не
используется
в
стандартной
реализации.
Как
правило,
метод
Глава
28.
Служебные
средства параллелизма
1121
вательного
выполнения
задач.
Обычно
лучше
ошибиться
в
большую,
чем
в
меньшую
сторону.
Если
пороговое
значение
слишком
мало,
на
формирование
и
переключение
задач
может
уйти времени
больше,
чем
на
их
обработку.
Во-вторых,
обычно
лучше
выбирать
уровень
параллелизма,
устанавливаемый
по
умолчанию.
Если
же указать
меньший уровень
параллелизма,
это
может
в
значительной
степени
свести
на
нет
все
преимущества,
которые
дает
применение
каркаса
Fork/Join Framework.
Обычно
в
задаче
типа
ForkJoinTask
не
должны
применяться
синхронизи
рованные
методы
или
блоки
кода.
Кроме того,
метод
compute
()
обычно
при
меняется
вместе
с
другими
средствами
синхронизации,
например
семафорами.
(Тем
не
менее
можно
воспользоваться
новым
классом
Phaser,
поскольку
он
со
вместим
с
механизмом
вилочного
соединения.)
Напомним,
что
в
основу
класса
ForkJoinTask
положена
стратегия
"разделяй
и
властвуй':
Но
такой
подход
обыч
но
не
применяется
в
тех
случаях,
когда
требуется
внешняя синхронизация. Кроме
того,
старайтесь
избегать
ситуаций,
когда
ввод-вывод
может
привести
к
длитель
ной
блокировке.
В
подобных
случаях
класс
ForkJoinTask
обычно
не
обслужи
вает
ввод-вывод.
Проще
говоря,
задача
должна
выполнять
вычисление,
которое
может
быть
организовано
без
внешней
блокировки
или
синхронизации.
Это
по
зволит
лучше
воспользоваться
преимуществами
каркаса
Fork/Join Framework.
И
последнее
замечание:
за
исключением
необычных
обстоятельств
не делайте
никаких
предположений
относительно
среды
выполнения,
в
которой
будет
рабо
тать
написанный
вами
прикладной
код.
Это
означает,
что вы
не
должны
предпо
лагать,
что
будет
доступно
конкретное
количество
процессоров
или
что
на
харак
теристики
выполнения
вашей
прикладной
программы
не
будут
оказывать
влияние
другие
одновременно
выполняющиеся
процессы.
Служебные
средства
параллелизма
в сравнении с традиционным
подходом к многозадачности
в
Java
Принимая
во
внимание
эффективность
и
гибкость
служебных
средств
парал
лелизма,
естественно
задаться
следующим
вопросом:
заменяют
ли
они
собой
традиционный подход в
многозадачности,
принятый в
Java?
Безусловно,
не
заме
няют!
Первоначальная
поддержка
многозадачности
и
встроенные
средства
син
хронизации
по-прежнему
должны
применяться
во
многих
прикладных
програм
мах
на
Java.
Например,
ключевое
слово
synchronized,
а
также
методы
wai
t
()
и
not
i
fy
( )
предоставляют
изящные
решения
широкого
ряда
задач
многопо
точной
обработки.
Но
когда
требуется
дополнительное
управление
потоками
ис
полнения,
на
помощь
приходят
служебные
средства
параллелизма.
Кроме
того,
в
каркасе
Fork/Join Framework
предоставляется
эффективный
способ,
позволяю
щий
интегрировать
методики
параллельного
программирования
в
более
слож
ные
прикладные
программы.
1130
Часть
11.
Библиотека
Java
Ниже
приведен
результат,
выводимый
данной
программой.
Исходный |
список: |
[7, |
18, |
10, |
|
24, |
|
17, |
|||
Минимальное |
значение: |
|
5 |
|
|
|
|
|
|||
Максимальное |
значение: |
24 |
|
|
|
|
|
||||
Отсортированный |
поток |
|
данных: |
5 |
7 |
10 |
|||||
Нечетные |
значения: |
5 |
7 |
17 |
|
|
|
|
|
||
Нечетные |
значения |
больше |
5: |
7 |
17 |
|
|
||||
5)
17
18
24
Рассмотрим
каждую
потоковую
операцию
из
данной
программы
в
отдельности.
После
создания
списочного
массива
типа
ArrayList
в
данной
программе
вызы
вается
метод
stream
()
для
получения
потока
элементов
этого массива:
Stream<Integer>
myStream
=
myList.stream();
Как
пояснялось
ранее,
в
интерфейсе
Collection
теперь
определяется
метод
stream
()
для
получения
потока
данных
из
вызывающей
коллекции.
Благодаря
тому
что
интерфейс
Collection
реализуется
классом
каждой
коллекции,
вы
звав
метод
s
tream
(),можно
получить
поток
данных
для
коллекции
любого
типа,
в
том
числе
и
ArrayList.
В
данном
случае
ссылка
на
получаемый
поток данных
присваивается
переменной
экземпляра
myStream.
Далее
в
рассматриваемой
здесь
программе
получается
минимальное
значение,
обнаруживаемое точнике данных).
в потоке данных (разумеется, оно является Полученное минимальное значение затем
минимальным отображается,
и в как
ис по
казано
ниже.
Optional<Integer> minVal = myStream.min(Integer::compare); |
|
if(minVal.isPresent()) |
System.out.println( |
Минимальное |
значение: " + minVal.get() ); |
Как
следует
из
табл.
29.2,
метод
min
()
объявляется
следующим
образом:
Optional<T>
min(Comparator<?
super
Т>
компаратор)
Обратите
прежде
всего
внимание
на
параметр
компаратор
метода
min
().
Обозначаемый
им
компаратор
служит
для
сравнения
двух
элементов
в
потоке
данных.
В
данном
примере
методу
min
( )
передается
ссылка
на
метод
compare
( )
из
клас
са
Integer.
Этот
метод
служит
для
реализации
компаратора
типа
Comparator,
способного
сравнивать
два
объекта
типа
Integer.
Обратите
далее
внимание
на
то,
что
метод
min
()
возвращает
объект
типа
Optional.
Класс
Optional
под
робно
описывается
в
главе
20,
а
здесь
вкратце
поясняется
принцип
его
действия.
Этот
класс
является
обобщенным,
входит
в
состав
пакета
j
ava.
u
t
i
1
и
объявля
ется
следующим
образом:
class
Optional<T>
Здесь
параметр
Т
обозначает
тип
элемента.
Экземпляр
класса
Optional
мо
жет
содержать значение
типа
т
или
же быть
пустым.
Вызвав
метод
isPresent
(),
можно
определить,
присутствует
ли
значение
в
данном
объекте.
Если
значение
присутствует,
его
можно
получить,
вызвав
метод
get
().
В
данном
примере
воз
вращается
объект,
содержащий
минимальное
значение
из
потока
данных
в
виде
объекта
типа
Integer.
Глава
29.
Потоковый
прикладной
интерфейс
API
1133
В интерфейсе Stream определяются |
три варианта |
метода reduce |
(). |
||
приведены общие формы двух из них. |
|
|
|
|
|
Optional<T> |
reduce(BinaryOperator<T> |
накопитель) |
|
|
|
Т reduce(T |
значение_идентичности, BinaryOperator<T> |
накопитель) |
|
||
Ниже
В
первой
форме
возвращается
объект
типа
Optional,
содержащий
полученный
результат,
а
во
второй
форме
-
объект типа
т,
т.е.
типа элемента
из
потока
данных.
В
обеих
формах
указанный
накопитель
обозначает
функцию,
оперирующую
дву
мя
значениями
и
получающую
результат.
Во
второй
форме
параметр
значение_
идентичности
обозначает
такое
значение,
что
операция
накопления,
включающая
зна
чение_идентичности
и
любой
элемент
из
потока
данных,
дает
в
итоге
тот
же
самый
элемент
без
изменения.
Так,
если
выполняется
операция
сложения,
то
значе
ние идентичности равно нулю, поскольку О |
+ |
х = х. А если выполняется |
|
умножения, то значение идентичности равно 1, |
поскольку 1 |
* х = х. |
|
операция
Интерфейс
BinaryOperator
является
функциональным
и
объявляется
в
пакете
java.util.function.
Он
расширяет
функциональный
интерфейс
BiFunction.
В
интерфейсе
В
i
Func
t
i
on
определяется
следующий
абстрактный
метод:
R
apply
(Т
значение
1
,
U
значение)
Здесь
параметр
R
обозначает
тип
результата;
параметр
Т
-
тип
первого
операн
да;
параметр
U -
тип
второго
операнда.
Следовательно,
метод
apply
()
применя
ет
функцию
к
своим
операндам
(зна
чение
1
и
значение)
и
возвращает
результат.
Когда
функциональный
интерфейс
BinaryOperator
расширяет
функциональ
ный
интерфейс
BiFunction,
то
все
параметры
типа
в
нем
обозначают
один
и
тот
же
тип.
Следовательно,
в
интерфейсе
BinaryOperator
метод
apply
()
объявля
ется
следующим
образом:
Т
apply
(Т
значение
1
,
Т
значение
2
)
По
отношению
к
методу
reduce
()
параметр
значение
1
метода
apply
()
бу
дет содержать предыдущий результат, тогда как параметр |
зна чение2 |
- |
|
щий элемент. При первом вызове данного метода параметр значение |
2 |
||
|
|||
следую будет со
держать
значение
идентичности
или
первый
элемент
в
зависимости
от
применяе
мого варианта метода reduce (). |
|
Следует, однако, иметь в виду, |
что операция |
трем ограничениям. Она должна быть: |
|
накопления
должна
удовлетворять
• • •
без сохранения состояния; без вмешательства; ассоциативная.
Как
пояснялось
ранее,
операция
без
сохранения
состояния
означает,
что
она
опирается
на
сведения
о
состоянии.
Следовательно,
каждый
элемент
из
потока
данных
обрабатывается
отдельно.
Операция
без
вмешательства
означает,
что
ис
точник
данных
не
видоизменяется
самой
операцией.
И
наконец,
операция
должна
быть
ассо11иативной.
В
данном
случае
понятие
ассоциативная
операция
употре-
1134
Часть
11.
Библиотека
Java
бляется
в
его
обычном
для
арифметики
значении.
Это
означает,
что
если
ассоциа
тивный оператор
применяется
в
последовательности
операций,
то
не
имеет
ника
кого
значения,
какая
именно
пара
операндов
обрабатывается первой.
Например,
вычисление
следующего
выражения:
(10 дает 10 *
* |
2) |
такой |
|
(2 |
* |
* же 7)
7 результат,
как
и
вычисление
приведенного
ниже
выражения.
Ассоциативность
имеет
особое
значение
для
правильного
применения
опера
ций
сведения
в
параллельных
потоках
данных,
обсуждаемых
в
следующем
разделе.
В
следующем
примере
программы
демонстрируется
применение
рассмотренных
выше
вариантов
метода
reduce
():
11
Продемонстрировать
применение метода
reduce()
import import
java.util.*; java.util.stream.*;
class StreamDemo2 { |
|
|
|
|
|
|
puЫic static |
void |
main(String[] |
args) |
{ |
||
11 создать |
список |
|
объектов |
типа |
Integer |
|
ArrayList<Integer> |
myList |
= new |
ArrayList<>( |
|||
);
myList.add(7); myList. add ( 18); myList.add(lO); myList.add(24); myList.add(17); myList.add(S);
11 11 11
Два способа получения результата перемножения |
||
целочисленных элементов |
списка myList |
с помощью |
метода reduce() |
|
|
Optional<Integer> productObj |
= |
|
|
|
myList.stream() .reduce( (а,Ь) |
-> |
а*Ь); |
||
if(productObj.isPresent()) |
|
|
|
|
System.out.println("Пpoизвeдeниe в |
виде |
объекта |
||
+ "типа Optional: |
" |
+ productObj.get()); |
||
"
int product |
= myList.stream() .reduce(l, |
(а,Ь) |
-> |
|||
System.out.println("Пpoизвeдeниe |
в |
|
виде |
значения |
||
|
+ "типа int: |
" |
+ |
product); |
|
|
а*Ь);
Как показано ниже, в обоих
ется одинаковый результат.
вариантах
применения
метода
reduce
( )
получа
Произведение Произведение
в в
виде виде
объекта |
типа |
значения |
типа |
Optional: 2570400
int: 2570400
Глава
29.
Потоковый
прикладной
интерфейс
API
1135
с
Сначала в данной программе применяется первый |
вариант метода |
reduce () |
лямбда-выражением для получения произведения |
двух числовых |
значений. |
В
связи
с
тем
что
поток
в
данном
примере
содержит
объекты
типа Integer,
они
автоматически распаковываются
перед
операцией
умножения и
снова
упаковы
ваются
перед
возвратом
результата.
Оба
перемножаемых
значения
представляют
текущий
результат
и
следующий
элемент
в
потоке
данных.
А
конечный
результат
возвращается
в
виде
объекта
типа
Optional.
Выводимое
значение
получается
в
результате
вызова
метода
get ()
для
возвращаемого
объекта.
Далее
в
данной
программе
применяется
второй
вариант
метода
reduce
(),при
вызове
которого
значение
идентичности
указывается
явным
образом:
для
опера
ции
умножения
оно
равно
1.
Обратите
внимание
на
то,
что
результат
возвраща
ется
в
виде
объекта,
тип
которого
соответствует
типу элемента
из
потока
данных
(в данном случае - |
Integer). |
Столь простые |
операции сведения, |
как
умножение,
удобны
в
качестве
приме
ров,
но
этим
применение
операций
сведения,
конечно,
не
ограничивается.
Так,
если
обратиться
к
предыдущему
примеру
программы,
то
получить
произведение
только
четных
целочисленных
значений
можно
следующим
образом:
int
evenProduct
=
myList.stream() |
|
if(b%2 |
== 0) |
} ) ; |
|
.reduce(l, return а*Ь;
(а,Ь) else
-> { return
а;
Обратите особое внимание на лямбда-выражение. Если |
|
ное числовое значение, то возвращается произведение а |
* |
параметр Ь |
имеет чет |
Ь, а иначе - |
значение |
параметра
а.
И
это
вполне
допустимо,
поскольку
параметр
а
содержит
текущий
ре
зультат,
а
параметр
Ь
-
следующий
элемент
из потока данных,
как
пояснялось
ранее.
Параллельные
потоки данных
Прежде
чем
продолжить
рассмотрение
потокового
прикладного
интерфейса
API,
следует
остановиться
на
параллельных
потоках
данных.
Как
отмечалось
ра
нее
в
данной
книге,
параллельное
выполнение
кода
на
многоядерных
процессорах
позволяет
добиться
значительного
повышения
производительности.
Вследствие
этого
параллельное
программирование
стало
важной
частью
современного
ар
сенала средств программистов.
Но
в
то
же
время
параллельное
программирова
ние
-
дело
непростое
и
чреватое
ошибками.
Поэтому
одно
из
преимуществ би
блиотеки
потоков
данных
заключается
в
том,
что
она позволяет
просто
и
надежно
организовать параллельное |
выполнение некоторых операций. |
Запросить параллельную |
обработку потока данных совсем |
не
трудно.
Для
это
го
достаточно
воспользоваться
параллельным
потоком
данных.
Как
упоминалось
ранее,
чтобы
получить
параллельный
поток
данных,
можно,
в
частности,
вызвать
метод
par
а
11е1
S t
ream
( ) ,
определенный в
классе
Со
11
ее
t
i
оп.
С
другой
сторо
ны,
можно
вызвать
метод
par
а
11е1
( )
в
последовательном
потоке данных.
Метод
paral
lel
()
определяется
в
интерфейсе
BaseStream
следующим
образом:
S
parallel()
