Что такое барьер в канкаренси
Что такое барьер в канкаренси
— Сегодня будет небольшая, но интересная и полезная тема – сортировки коллекций.
— Сортировка? Я что-то про это слышал.
— Давным-давно каждый программист обязан был уметь писать сортировку. Умел и писал. Но те времена канули в лету. Сегодня написание своей сортировки считается дурным тоном, как и написание всего, что уже было придумано.
В Java (да и других языках программирования) сортировки уже реализованы. Твоя задача – научиться правильно пользоваться тем, что есть.
— У вспомогательного класса Collections есть статический метод sort, который используется для сортировки коллекций, а если точнее – списков. Элементы в коллекциях Map и Set не имеют порядка/номера, значит, и сортировать там нечего.
— Да, я вспомнил, я когда-то уже использовал этот метод для сортировки списка чисел.
— Отлично. Но этот метод гораздо мощнее чем, кажется на первый взгляд. Он может сортировать не только числа, но и любые объекты, по любым критериям. И помогают ему в этом два интерфейса: Comparable и Comparator.
Иногда бывает нужно отсортировать объекты, а не числа. Например, у тебя есть список людей, и ты хочешь отсортировать их по возрасту. Для этого есть интерфейс Comparable.
Давай я сначала покажу тебе пример, и все станет понятнее:
Пример |
---|
public class Woman implements Comparable < public int age; public Woman(int age) < Collections.sort(women); |
Чтобы объекты можно было сортировать, сначала нужно научиться их сравнивать. Для этого и используется Comparable. Интерфейс Comparable является generic’ом – т.е. типом с параметром. У него всего один generic-метод – compare(To). В этом методе и происходит сравнение переданного объекта и текущего (this). Т.е. надо переопределить этот метод в своем классе и сравнить в нем текущий объект (this) с переданным.
— А как работает compare? Я думал, что он будет возвращать true/false в зависимости от того – больше переданный объект или меньше.
— Тут все немного хитрее. Метод compare возвращает не true/false, а значение типа int. На самом деле так сделано для простоты.
Когда компьютеру нужно определить больше ли одно число, чем другое, он просто вычитает из первого числа второе, а потом смотрит, что получилось. Если 0 – числа равны, если получилось число меньше нуля, то второе число больше, а если результат больше нуля, то больше уже первое число.
Тут используется та же логика. Согласно спецификации метод compare должен вернуть ноль, если сравниваемые объекты равны. Если метод compare вернул число больше нуля, это значит, что наш (this) объект больше, чем переданный. Если метод compare вернул число меньше нуля, то объект this меньше чем переданный.
— Да, но если ты сравниваешь объекты просто по какому-то параметру-числу, то можешь просто вернуть разницу между ними – вычесть один из другого. Как это и сделано в примере выше.
— Вроде все понятно. Хотя может и не все. Но почти все.
— Отлично. Теперь рассмотрим более практическую задачу. Ты написал крутой сайт по пошиву женской одежды в Китае. Для описания своих пользователей ты используешь класс Woman. Ты даже сделал страницу с таблицей, где можешь посмотреть их всех. Но есть проблема…
Объект Woman содержит у тебя не только возраст, а еще целую кучу данных: имя, фамилию, рост, вес, количество детей, …
В таблице пользователей есть много колонок, и тут встает вопрос: а как сортировать пользователей по разным критериям? По весу, по возрасту, по фамилии?
— Гм. Действительно, часто вижу таблицы с сортировкой колонок. И как это сделать?
— А для этого есть второй интерфейс, о котором я хотел тебе сегодня рассказать – это интерфейс Comparator. И у него тоже есть метод compare, только он принимает не один параметр, а два. Вот как это работает:
Пример |
---|
public class Woman < public int age; public int childrenCount; public int weight; public int height; public String name; public Woman(int age) < |
Пример использования: |
public stati void main(String[] args ) < ArrayList women = new ArrayList (); women.add(new Woman(18)); women.add(new Woman(21)); women.add(new Woman(5)); Collections.sort(women, compareByHeight ); |
При использовании интерфейса Comparator, логика сравнения пары объектов не прячется внутрь класса/объекта, а реализуется в отдельном классе.
— Т.е. я могу сделать несколько классов, реализующих интерфейс Comparator, но в каждом из них сравнивать разные параметры? В одном – weight, в другом – age, в третьем – height?
— Да, это очень просто и удобно.
Мы просто вызываем метод Collections.sort, передаем туда список объектов и еще специальный объект во втором параметре, который реализует интерфейс Comparator и говорит, как правильно сравнивать пары объектов в процессе сортировки.
— Гм. Вроде все понятно. Дай-ка я сам попробую. Допустим, мне нужно отсортировать пользователей по весу, это будет так:
Collections.sort(women, compareByWeight );
— Отлично. А если я хочу отсортировать в обратном порядке?
— А подумать? Ответ очень простой!
— А если я хочу сортировать по фамилии? Как сортировать строки, Билаабо?
— А у строк уже реализован метод compare, надо просто вызвать его:
Пример кода, пользователи сортируются по имени: |
---|
Comparator compareByName = new Comparator () < public int compare (Woman o1, Woman o2) < return o1.name. compareTo (o2.name); > >; Collections.sort(women, compareByName ); |
— Это был отличный урок, Билаабо, спасибо тебе большое.
— И тебе спасибо, друг!
2. Задачи на сортировку и comparator
Задачи | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1. Почитать в инете про медиану выборки3. Разделяемые ресурсы, Конфликты, Проблема совместного доступа— Привет, Амиго! Хочу тебе рассказать о совместном использовании ресурсов. Разными нитями, ясное дело. Я все время говорю о проблемах при работе нескольких нитей и о том, как их решать. Это не значит, что использование нитей – это плохо. Нити – это очень мощный инструмент. Фактически, они позволяют увеличить скорость, и даже надежность работы твоей программы. Чем сложнее программа – тем больше в ней нитей и различных самостоятельных частей. Разбиение программы на независимые (слабосвязанные) части очень выгодно. Представь, что твоя программа внутри разбита на 100 нитей. Но у тебя всего двухъядерный процессор. Это значит, что на каждом ядре исполняется в среднем 50 нитей. Если же тебе нужно нарастить мощность программы, ты просто покупаешь двухпроцессорный сервер и пару крутых процессоров для него. В результате у него суммарно может быть до 32- ядер, и производительность твоей программы может вырасти в 2-20 раз. В зависимости от того, насколько действительно независимых частей она разбита. Это одна из причин доминирования Java в секторе Enterprise-разработки. Если у компании есть сложная программа для внутренних нужд, которую пишут 20 разработчиков, то купить еще один сервер гораздо дешевле, чем ускорить программу в 2 раза. — Так вот оказывается в чем дело. — Но! Каждый раз, когда разработчик решает использовать еще одну нить, он решает одну проблему, а создает две. Слишком много нитей не увеличат производительность программы до бесконечности. Во-первых, в любой программе есть работа, которую невозможно разбить на части и выполнять параллельно в разных нитях. Во-вторых, все нити выполняются на одном и том же процессоре. Чем больше нитей, тем медленнее работает каждая из них. И, самое главное – нити часто используют одни и те же объекты (их обычно называют разделяемыми ресурсами). Например, нить хочет сохранить в файл информацию о сделанной работе. Если таких нитей несколько, и они хотят записать информацию в один файл – они будут мешать друг другу. Чтобы в файле не было мешанины, каждая нить пользуется уникальным доступом к файлу – т.е. пока файлом пользуется одна нить, другие ждут. — Да, я помню, это делается с помощью ключевого слова synchronized. — А если нити пишут в разные файлы? — Формально – это разные объекты, но жесткий диск же один. — Т.е. реально что-то распараллелить можно только внутри процессора? — Формально – Да, как только твоей нити надо что-то еще кроме данных, которые у нее есть, это что-то уже может быть занято другой нитью и придется ждать. — И что же делать? Как узнать – делать много нитей или нет? — Это определяется непосредственно архитектурой программы. У любого проекта есть его «архитектор», который знает все «ресурсы», которые используются в программе, знает их ограничения, и насколько они хорошо/плохо параллелятся. — Тут есть два варианта: а) поработать под началом такого специалиста б) набить шишек самому — Я – робот, у меня не бывает шишек – только вмятины. — Ну, значит, набить вмятин. — Ясно, спасибо. Ты прояснила некоторые вопросы, о которых я уже начал ломать голову. Многопоточный пакет util.concurrentРазработка многопоточного кода, обеспечивающего не только хорошую производительность, но и защиту данных приложения, не является тривиальной задачей. Именно для таких целей предназначен пакет java.util.concurrent. Разработчики, как правило, ограничиваются обычной синхронизацией, и поэтому с пакетом многопоточности java.util.concurrent они сталкиваются не так уж часто. В данной статье приводится обзор пакета java.util.concurrent, разбитого на несколько функциональных групп. В описании каждой группы приводится ссылка на более подробное описание объектов с примерами на страницах сайта. Дополнительную информацию о многопоточности можно найти в документации Oracle Java Concurrency Tutorials и в книге Doug Lea «Concurrent Programming in Java by Doug Lea», являющегося автором пакета java.util.concurrent и профессором университета штата Нью Йорк. К наиболее известным java разработкам Doug Lea следует отнести collections и util.concurrent, которые в том или ином виде отразились в существующих JDK. На домашней страничке Doug Lea размещена второе издание книги по многопоточности Concurrent Programming in Java™: Design Principles and Pattern, 2nd Edition, с которой можно познакомиться здесь. Структура пакета java.util.concurrentКлассы и интерфейсы пакета java.util.concurrent объедининены в несколько групп по функциональному признаку, как это представлено в следующей таблице :
Concurrent CollectionsОбычные наборы данных, реализующих интерфейсы List, Set и Map, нельзя использовать в многопоточных приложениях, если требуется синхронизация, т.е. такие коллекции недопустимы для одновременного чтения и изменения данных разными потоками. Методы обрамления Collections framework (synchronizedList, synchronizedSet, synchronizedMap), появившиеся в JDK 1.2, имеют существенный недостаток, связанный с препятствованием масштабируемости, поскольку с коллекцией одновременно может работать только один поток. Пакет java.util.concurrent предлагает свой набор потокобезопасных классов, допускающих разными потоками одновременное чтение и внесение изменений. Итераторы классов данного пакета представляют данные на определенный момент времени и не вызывают исключение ConcurrentModificationException. Все операции по изменению коллекции (add, set, remove) приводят к созданию новой копии внутреннего массива. Этим гарантируется, что при проходе итератором по коллекции не будет ConcurrentModificationException. Следует помнить, что при копировании массива копируются только ссылки на объекты. CopyOnWriteArrayList реализует алгоритм CopyOnWrite и является потокобезопасным аналогом ArrayList. Класс CopyOnWriteArrayList содержит изменяемую ссылку на неизменяемый массив, обеспечивая преимущества потокобезопасности без необходимости использования блокировок. Т.е. при выполнении модифицирующей операции CopyOnWriteArrayList создаёт новую копию списка и гарантирует, что её итераторы вернут состояние списка на момент создания итератора и не вызовут ConcurrentModificationException. Описание CopyOnWriteArrayList с примером представлено здесь. ConcurrentHashMap реализует (implements) интерфейс java.util.concurrent.ConcurrentMap и отличается от HashMap и Hashtable внутренней структурой хранения пар key-value. СoncurrentHashMap использует несколько сегментов, и данный класс можно рассматривать как группу HashMap’ов. По умолчанию количество сегментов равно 16. Доступ к данным определяется по сегментам, а не по объекту. Итераторы данного класса фиксируют структуру данных на момент начала его использования. Описание ConcurrentHashMap с примером представлено здесь. CopyOnWriteArraySet выполнен на основе CopyOnWriteArrayList с реализацией интерфейса Set. Описание CopyOnWriteArraySet с примером представлено здесь. ConcurrentNavigableMap расширяет возможности интерфейса NavigableMap для использования в многопоточных приложениях; итераторы класса декларируются как потокобезопасные и не вызывают ConcurrentModificationException. ConcurrentSkipListMap является аналогом коллекции TreeMap с сортировкой данных по ключу и с поддержкой многопоточности. ConcurrentSkipListSet выполнен на основе ConcurrentSkipListMap с реализацией интерфейса Set. Concurrent Synchronizers, объекты синхронизацииПрежде чем говорить о синхронизаторах пакета java.util.concurrent, вспомним, что такое синхронизация. Синхронизация — это процесс, позволяющий выполнять в программе синхронно параллельные потоки. Несколько потоков могут мешать друг другу при обращении к одним и тем же объектам приложения. Для решения этой проблемы используется мьютекс, он же монитор, имеющий два состояния — объект занят и объект свободен. Монитор (мьютекс) — это высокоуровневый механизм взаимодействия и синхронизации процессов, обеспечивающий доступ к неразделяемым ресурсам. Мьютекс встроен в класс Object и, следовательно, имеется у каждого объекта. Синхронизация в Java реализуется использованием зарезервированного слова synchronized. Можно использовать synchronized в классах, определяя синхронизированные методы или блоки. Но нельзя использовать synchronized в переменных или атрибутах в определении класса. Таким образом, при обычной синхронизации потоков используют оператор synchronized для ограничения (блокирования) доступа к определенному методу, блоку кода или объекту без каких-либо условий. Пакет java.util.concurrent содержит пять объектов синхронизации, позволяющих накладывать определенные условия для синхронизации потоков. Semaphore (семафор) — объект синхронизации, ограничивающий одновременный доступ к общему ресурсу нескольким потокам с помощью счетчика. При запросе разрешения и значении счетчика больше нуля доступ предоставляется, а счетчик уменьшается; в противном случае — доступ запрещается. При освобождении ресурса значение счетчика семафора увеличивается. Количество разрешений семафора определяется в конструкторе. Второй конструктор семаформа включает дополнительный параметр «справедливости», определяющий порядок предоставления разрешения ожидающим доступа потокам. Описание с примером представлено здесь. CountDownLatch («защелка с обратным отсчетом») — объект синхронизации потоков, блокирующий один или несколько потоков до тех пор, пока не будут выполнены определенные условия. Количество условий задается счетчиком. При обнулении счетчика, т.е. при выполнении всех условий, блокировки выполняемых потоков будут сняты и они продолжат выполнение кода. Примером CountDownLatch может служить экскурсовод, собирающий группу из заданного количества туристов. Как только группа собрана она отправляется на экскурсию. Необходимо отметить, что счетчик одноразовый и не может быть инициализирован по-новому. Описание с примером представлено здесь CyclicBarrier — объект синхронизации типа «Барьер» используется, как правило, в распределённых вычислениях. Барьерная синхронизация останавливает участника (исполняемый поток) в определенном месте в ожидании прихода остальных потоков группы. Как только все потоки достигнут барьера, барьер снимается и выполнение потоков продолжается. Циклический барьер CyclicBarrier, также, как и CountDownLatch, использует счетчик и похож на него. Отличие связано с тем, что «защелку» нельзя использовать повторно после того, как её счётчик обнулится, а барьер можно использовать (в цикле). Описание с примером представлено здесь Exchanger — объект синхронизации, используемый для двустороннего обмена данными между двумя потоками. При обмене данными допускается null значения, что позволяет использовать класс для односторонней передачи объекта или же просто, как синхронизатор двух потоков. Обмен данными выполняется вызовом метода exchange, сопровождаемый самоблокировкой потока. Как только второй поток вызовет метод exchange, то синхронизатор Exchanger выполнит обмен данными между потоками. Описание с примером представлено здесь Phaser — объект синхронизации типа «Барьер», но, в отличие от CyclicBarrier, может иметь несколько барьеров (фаз), и количество участников на каждой фазе может быть разным. Описание с примером представлено здесь Атомарные классы пакета java.concurrent.atomicПакет java.util.concurrent.atomic включает девять атомарных классов для выполнения, так называемых, атомарных операций. Операция является атомарной, если её можно безопасно выполнять при параллельных вычислениях в нескольких потоках, не используя при этом ни блокировок, ни синхронизацию synchronized. Атомарный класс включает метод compareAndSet, реализующий механизм оптимистичной блокировки и позволяющий изменить значение только в том случае, если оно равно ожидаемому значению. Т.е. если значение атомарного класса было изменено в другом потоке, то оно не будет равно ожидаемому значению, и метод compareAndSet не позволит изменить значение. Ряд архитектур процессоров имеют инструкцию Compare-And-Swap (CAS), которая реализует операцию compareAndSet. Таким образом, на уровне инструкций процессора имеется поддержка необходимой атомарной операции. В архитектурах процессоров, где инструкция не поддерживается, операции реализованы иными низкоуровневыми средствами. Подробнее об описании атомарных классов с примерами можно познакомиться здесь. QueuesПакет java.util.concurrent содержит классы формирования неблокирующих и блокирующих очередей для многопоточных приложений. Неблокирующие очереди «заточены» на скорость выполнения, блокирующие очереди приостанавливают потоки при работе с очередью. Неблокирующие очереди ConcurrentLinkedQueue реализуют интерфейс Queue и формирует неблокирующую и ориентированную на многопоточное исполнение очередь. Размер очереди ConcurrentLinkedQueue не имеет ограничений. Имплементация очереди использует wait-free алгоритм от Michael & Scott, адаптированный для работы с garbage collector’ом. Данный алгоритм довольно эффективен и очень быстр, т.к. построен на CAS (Compare-And-Swap). Описание с примером представлено здесь. ConcurrentLinkedDeque реализует интерфейс Deque (Double ended queue), читается как «Deck». Данная реализация позволяет добавлять и получать элемента с обеих сторон очереди. Соответственно, класс поддерживает оба режима работы : FIFO (First In First Out) и LIFO (Last In First Out). ConcurrentLinkedDeque следует использовать в том случае, если необходимо реализовывать LIFO, поскольку за счет двунаправленности данный класс проигрывает по производительности очереди ConcurrentLinkedQueue. Описание с примером представлено здесь. При обработке большого количества потоков данных использование неблокирующих очередей иногда может оказаться явно недостаточным : разгребающие очереди потоки перестанут справляться с наплывом данных, что может привести к «out of memory» или перегрузить IO/Net настолько, что производительность упадет в разы, пока не наступит отказ системы по таймаутам или из-за отсутствия свободных дескрипторов в системе. Самое неприятное в данном случае то, что возникающая ситуация является нестабильной, сложной для отладки. Для таких случаев нужна блокирующая очередь с возможностью задать её размер и/или условия блокировки. Интерфейсы блокирующих очередей наряду с возможностью определения размера очереди включают методы, по-разному реагирующие на незаполнение или переполнение queue. Так, например, при добавлении элемента в переполненную очередь, один из методов вызовет IllegalStateException, другой вернет false, третий заблокирует поток, пока не появится место, четвертый же заблокирует поток на определенное время (таймаут) и вернет false, если место так и не появится. ArrayBlockingQueue — блокирующая очередь, реализующая классический кольцевой буфер. Параметр fair в конструкторе позволяет управлять справедливостью очереди для упорядочивания работы ожидающих потоков производителей (вставляющих элементы) и потребителей (извлекающих элементы). Описание с примером представлено здесь. LinkedBlockingQueue — блокирующая очередь на связанных узлах, реализующая «two lock queue» алгоритм : один lock добавляет элемент, второй извлекает. За счет двух lock’ов данная очередь показывает более высокую производительность по сравнению с ArrayBlockingQueue, но и расход памяти повышается. Размер очереди задается через конструктор и по умолчанию равен Integer.MAX_VALUE. Описание с примером представлено здесь. LinkedBlockingDeque — двунаправленная блокирующая очередь на связанных узлах, реализованная как простой двунаправленный список с одним локом. Размер очереди задается через конструктор и по умолчанию равен Integer.MAX_VALUE. Описание с примером представлено здесь. SynchronousQueue — блокирующая очередь, в которой каждая операция добавления должна ждать соответствующей операции удаления в другом потоке и наоборот. Т.е. очередь реализует принцип «один вошел, один вышел». SynchronousQueue не имеет никакой внутренней емкости, даже емкости в один элемент. Описание с примером представлено здесь. LinkedTransferQueue — блокирующая очередь с реализацией интерфейса TransferQueue, который позволяет при добавлении элемента в очередь заблокировать вставляющий поток до тех пор, пока другой поток не заберет элемент из очереди. Блокировка может быть как с таймаутом, так и с проверкой ожидающего потока. Таким образом может быть реализован механизм передачи сообщений с поддержкой как синхронных, так и асинхронных сообщений. Подробное описание представлено здесь. DelayQueue — специфичный вид очереди, позволяющий извлекать элементы только после некоторой задержки, определенной в каждом элементе через метод getDelay интерфейса Delayed. Подробное описание представлено здесь. PriorityBlockingQueue — является многопоточной оберткой интерфейса PriorityQueue. При размещении элемента в очереди, его порядок определяется в соответствии с «натуральным» упорядочиванием, либо логикой Comparator’а или имплементации Comparable интерфейса. Подробное описание представлено здесь. Описание пакета java.concurrent.locksПакет java.util.concurrent.locks включает классы, которые существенно отличаются от встроенной синхронизации и мониторов, и которые можно использовать для блокировки ресурсов с определенными условиями. Этот пакет дает намного большую гибкость в использовании блокировок без условий и с условием. Lock — базовый интерфейс, предоставляющий более гибкий подход при ограничении доступа к ресурсам/блокам по сравнению с использованием synchronized. Так, при использовании нескольких блокировок, порядок их освобождения может быть произвольный. Имеется возможность перехода к альтернативному сценарию, если блокировка уже захвачена. Более подробное описание Lock с примером представлено здесь. Condition — интерфейсное условие в сочетании с блокировкой Lock позволяет заменить методы монитора/мьютекса (wait, notify и notifyAll) объектом, управляющим ожиданием событий. Объект с условием чаще всего получается из блокировок с использованием метода lock.newCondition(). Таким образом можно получить несколько комплектов wait/notify для одного объекта. Блокировка Lock заменяет использование synchronized, а Condition — объектные методы монитора. Более подробное описание Condition с примером представлено здесь. ReadWriteLock — интерфейс создания read/write блокировок, который реализует один единственный класс ReentrantReadWriteLock. Блокировку чтение-запись следует использовать при длительных и частых операциях чтения и редких операциях записи. Тогда при доступе к защищенному ресурсу используются разные методы блокировки, как показано ниже : Более подробное описание интерфейса ReadWriteLock здесь. Описание ExecutorsМногопоточный пакет concurrent включает средства, называемые сервисами исполнения, позволяющие управлять потоковыми задачами с возможностью получения результатов через интерфейсы Future и Callable. Callable — расширенный аналог интерфейса Runnable, позволяющий возвращать типизированное значение. В интерфейсе используется метод call, являющийся аналогом метода run интерфейса Runnable. Более подробное описание интерфейса Callable с примером представлено здесь. FutureTask — класс-оболочка, базирующаяся на конкретной реализации интерфейса Future. Чтобы создать реализацию класса FutureTask необходим объект Callable. FutureTask представляет удобный механизм для превращения Callable одновременно в Future и Runnable, реализуя оба интерфейса. Имплементация FutureTask может быть передана на выполнение классу, реализующему интерфейс Executor, либо запущена в отдельном потоке, как класс, реализующий интерфейс Runnable. Более подробное описание интерфейса FutureTask с примером представлено здесь. ExecutorService — представляет собой интерфейс, имплементация которого используется для запуска потоков. Потоки можно запускать, используя методы execute и submit. Оба метода в качестве параметра принимают объекты Runnable или Callable. Метод submit возвращает значение типа Future, позволяющий получить результат выполнения потока. Метод invokeAll работает со списками задач с блокировкой потока до завершения всех задач в переданном списке или до истечения заданного времени. Метод invokeAny блокирует вызывающий поток до завершения любой из переданных задач. Реализация данного интерфейса включает метод shutdown, позволяющий завершить все принятые на исполнение задачи и блокирует поступление новых. Интерфейс ScheduledExecutorService расширяет свойства ExecutorService для поддержки планирования потоков исполнения. В пакет concurrent включены три предопределенных класса исполнителей: ThreadPoolExecutor, ScheduledThreadPoolExecutor и ForkJoinPool. Чтобы получить реализацию данных объектов необходимо использовать класс-фабрику Executors. Описание интерфейса ExecutorService с примером представлено здесь
|