Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
105
Добавлен:
30.03.2021
Размер:
42.12 Mб
Скачать

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()

Соседние файлы в папке Лабораторные работы (Объектно-ориентированное программирование (ООП))