Руководство по использованию параллельного пакета java.util.concurrent
Преамбула
Поскольку эта статья является кратким введением в соответствующие инструменты разработки в пакете java.util.concurrent, я помогу вам понять классы в этом пакете и попытаться использовать их в проекте.
В этой статье не будут объясняться основные вопросы параллелизма в Java — принципы, лежащие в его основе, то есть, если вы заинтересованы в этих вещах, обратитесь к "Руководство по параллелизму Java".
Пожалуйста, будьте терпеливы, когда обнаружите пропущенные классы или интерфейсы. Они будут добавлены, когда автор будет свободен.
содержание
[TOC]
1. Очередь на блокировку
Интерфейс BlockingQueue в пакете java.util.concurrent представляет собой очередь, в которую поток помещает и извлекает экземпляры. В этом разделе я покажу вам, как использовать эту BlockingQueue.
1.1 Использование BlockingQueue
BlockingQueue обычно используется в сценариях, где один поток создает объекты, а другой поток использует эти объекты. Следующий рисунок иллюстрирует этот принцип:
В него ставится поток, а другой поток берет из него BlockingQueue.
Поток будет продолжать создавать новые объекты и вставлять их в очередь до тех пор, пока очередь не достигнет критической точки, в которой она может храниться. То есть он ограничен. Если очередь блокировки достигает своей критической точки, поток, отвечающий за производство, блокируется при вставке в него новых объектов. Он блокируется до тех пор, пока потребляющий поток не удалит объект из очереди.
Поток, ответственный за потребление, всегда будет брать объекты из очереди блокировки. Если поток-потребитель попытается извлечь объект из пустой очереди, поток-потребитель будет заблокирован до тех пор, пока поток-производитель не поместит объект в очередь.
1.2 Метод BlockingQueue
BlockingQueue имеет 4 разных набора методов для вставки, удаления и проверки элементов в очереди. Каждый метод также ведет себя по-разному, если запрошенная операция не может быть выполнена немедленно. Эти методы следующие:
действовать | генерировать исключение | конкретное значение | блокировать | тайм-аут |
---|---|---|---|---|
вставлять | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
Удалить | remove(o) | poll(o) | take(o) | poll(timeout, timeunit) |
экзамен | element(o) | peek(o) | недоступен | недоступен |
Объяснение четырех различных групп поведения:
- Генерация исключения: если предпринятая операция не может быть выполнена немедленно, генерируется исключение.
- Конкретное значение: возвращает определенное значение (обычно true/false), если предпринятая операция не может быть выполнена немедленно.
- Блокировка: если предпринятая операция не может быть выполнена немедленно, вызов метода будет заблокирован до тех пор, пока он не может быть выполнен.
- Тайм-аут: если предпринятая операция не может быть выполнена немедленно, вызов метода будет заблокирован до тех пор, пока ее можно будет выполнить, но не будет ждать дольше заданного значения. Возвращает конкретное значение, чтобы сообщить, была ли операция успешной (обычно true/false).
Невозможно вставить null в BlockingQueue. Если вы попытаетесь вставить нуль, BlockingQueue выдаст исключение NullPointerException. Все элементы в BlockingQueue доступны, а не только начальный и конечный элементы. Скажем, например, вы помещаете объект в очередь на обработку, но ваше приложение хочет его отменить. Затем вы можете вызвать такие методы, как remove(o), чтобы удалить определенные объекты из очереди. Но это не очень эффективно (Примечание переводчика: структуры данных на основе очередей, получение объектов, отличных от начальной или конечной позиции, будет не слишком эффективным), поэтому вы стараетесь не использовать этот тип метода, если только вам это действительно не нужно Не делайте тот.
1.3 Реализация BlockingQueue
BlockingQueue — это интерфейс, вам нужно использовать одну из его реализаций, чтобы использовать BlockingQueue. java.util.concurrent имеет следующую реализацию интерфейса BlockingQueue (Java 6):
- ArrayBlockingQueue
- DelayQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- SynchronousQueue
1.4 Примеры использования BlockingQueue в Java
Вот пример использования BlockingQueue в Java. В этом примере используется реализация ArrayBlockingQueue интерфейса BlockingQueue.
Во-первых, класс BlockingQueueExample запускает Producer и Consumer в двух отдельных потоках. Производитель вводит строки в общую очередь BlockingQueue, а потребитель извлекает их из нее.
public class BlockingQueueExample {
public static void main(String[] args) throws Exception {
BlockingQueue queue = new ArrayBlockingQueue(1024);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
Thread.sleep(4000);
}
} 123456789101112131415
Ниже приведен класс производителя. Обратите внимание, как он засыпает на секунду при каждом вызове put(). Это заставит Потребитель заблокироваться, ожидая объекта в очереди.
public class Producer implements Runnable{
protected BlockingQueue queue = null;
public Producer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
queue.put("1");
Thread.sleep(1000);
queue.put("2");
Thread.sleep(1000);
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} 1234567891011121314151617181920
Ниже находится потребительский класс. Он просто удаляет объекты из очереди и выводит их в System.out.
public class Consumer implements Runnable{
protected BlockingQueue queue = null;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} 123456789101112131415161718
2. Очередь блокировки массива
Класс ArrayBlockingQueue реализует интерфейс BlockingQueue.
ArrayBlockingQueue — это ограниченная блокирующая очередь, внутренняя реализация которой заключается в помещении объектов в массив. Ограниченный также означает, что он не может хранить бесконечное количество элементов. Он имеет верхний предел количества элементов, которые могут храниться одновременно. Вы можете установить этот верхний предел при его инициализации, но после этого его нельзя будет изменить).
ArrayBlockingQueue внутренне хранит элементы в порядке FIFO (первым пришел, первым вышел). Головной элемент в очереди — это тот, у которого самое большое время среди всех элементов, а хвостовой элемент — тот, у которого самое короткое время.
Вот пример инициализации ArrayBlockingQueue при его использовании:
BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1");
Object object = queue.take(); 123
Ниже приведен пример BlockingQueue с использованием дженериков Java. Обратите внимание, как размещаются и извлекаются элементы String:
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(1024);
queue.put("1");
String string = queue.take(); 123
3. Очередь задержки DelayQueue
DelayQueue реализует интерфейс BlockingQueue.
DelayQueue хранит элементы до тех пор, пока не истечет определенная задержка. Вводимые в него элементы должны реализовывать интерфейс java.util.concurrent.Delayed, который определяет:
public interface Delayed extends Comparable<Delayed> {
public long getDelay(TimeUnit timeUnit);
} 12345
DelayQueue освобождает каждый элемент по истечении периода времени значения, возвращаемого методом getDelay() каждого элемента. Если он возвращает 0 или отрицательное значение, задержка будет считаться просроченной, и элемент будет освобожден при следующем вызове взятия DelayQueue.
Экземпляр getDelay, передаваемый методу getDelay, представляет собой тип перечисления, указывающий период времени задержки. Перечисление TimeUnit будет принимать следующие значения:
- DAYS
- HOURS
- MINUTES
- SECONDS
- MILLISECONDS
- MICROSECONDS
- NANOSECONDS
Как видите, интерфейс Delayed также наследует интерфейс java.lang.Comparable, что означает возможность сравнения объектов Delayed. Это может быть полезно при сортировке элементов в DelayQueue, чтобы их можно было освобождать в порядке, основанном на сроке их действия.
Вот пример использования DelayQueue:
public class DelayQueueExample {
public static void main(String[] args) {
DelayQueue queue = new DelayQueue();
Delayed element1 = new DelayedElement();
queue.put(element1);
Delayed element2 = queue.take();
}
} 123456789
DelayedElement — это класс реализации созданного мной интерфейса DelayedElement, которого нет в пакете java.util.concurrent. Вам нужно создать собственную реализацию интерфейса Delayed, чтобы использовать класс DelayQueue.
4. Связанная очередь блокировки
Класс LinkedBlockingQueue реализует интерфейс BlockingQueue.
LinkedBlockingQueue внутренне хранит свои элементы в связанной структуре (узел связи). При желании для этой цепной структуры можно выбрать верхний предел. Если верхний предел не определен, в качестве верхнего предела будет использоваться Integer.MAX_VALUE.
LinkedBlockingQueue внутренне хранит элементы в порядке FIFO (первым пришел, первым вышел). Головной элемент в очереди — это тот, у которого самое большое время среди всех элементов, а хвостовой элемент — тот, у которого самое короткое время.
Ниже приведен пример кода для инициализации и использования LinkedBlockingQueue:
BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
BlockingQueue<String> bounded = new LinkedBlockingQueue<String>(1024);
bounded.put("Value");
String value = bounded.take(); 1234
5. PriorityBlockingQueue с приоритетной очередью блокировки
Класс PriorityBlockingQueue реализует интерфейс BlockingQueue.
PriorityBlockingQueue — неограниченная параллельная очередь. Он использует те же правила упорядочения, что и класс java.util.PriorityQueue. В эту очередь нельзя вставлять нулевые значения.
Все элементы, вставленные в PriorityBlockingQueue, должны реализовывать интерфейс java.lang.Comparable. Таким образом, порядок элементов в этой очереди зависит от вашей собственной реализации Comparable.
Обратите внимание, что PriorityBlockingQueue не применяет какое-либо конкретное поведение для элементов с одинаковым приоритетом (compare() == 0). Также обратите внимание, что если вы получаете Iterator из PriorityBlockingQueue, Iterator не гарантирует, что он проходит элементы в порядке приоритета.
Вот пример использования PriorityBlockingQueue:
BlockingQueue queue = new PriorityBlockingQueue();
//String implements java.lang.Comparable
queue.put("Value");
String value = queue.take(); 1234
6. Синхронная очередь
Класс SynchronousQueue реализует интерфейс BlockingQueue.
SynchronousQueue — это специальная очередь, которая может содержать только один элемент за раз. Если в очереди уже есть элемент, поток, пытающийся вставить новый элемент в очередь, будет заблокирован до тех пор, пока другой поток не удалит элемент из очереди. Аналогичным образом, если очередь пуста, поток, пытающийся извлечь элемент из очереди, будет заблокирован до тех пор, пока другой поток не вставит в очередь новый элемент.
Соответственно, называть этот класс очередью — преувеличение. Это скорее место встречи.
7. Блокировка Deque
Интерфейс BlockingDeque в пакете java.util.concurrent представляет очередь, в которую потоки помещают и извлекают экземпляры. В этом разделе я покажу вам, как использовать BlockingDeque.
Класс BlockingDeque представляет собой двустороннюю очередь, которая блокирует поток, пытающийся вставить элемент, когда он не может вставить элемент, и блокирует поток, пытающийся извлечь его, когда он не может извлечь элемент.
deque — это сокращение от «Двусторонняя очередь». Итак, очередь — это очередь, в которую вы можете вставлять или извлекать элементы с любого конца.
7.1 Использование BlockingDeque
BlockingDeque можно использовать, когда поток является одновременно и производителем очереди, и ее потребителем. Если потоку-производителю необходимо вставить данные на обоих концах очереди, а потоку-потребителю необходимо удалить данные на обоих концах очереди, в это время также можно использовать BlockingDeque. Диаграмма BlockingDeque:
BlockingDeque — потоки могут вставлять и извлекать элементы на обоих концах очереди.
Поток создает элементы и вставляет их в любой конец очереди. Если очередь заполнена, поток вставки будет заблокирован до тех пор, пока поток удаления не удалит элемент из очереди. Если очередь пуста, поток удаления будет заблокирован до тех пор, пока поток вставки не вставит новый элемент в очередь.
7.2 Метод BlockingDeque
BlockingDeque имеет 4 разных набора методов для вставки, удаления и проверки элементов в очереди. Каждый метод также ведет себя по-разному, если запрошенная операция не может быть выполнена немедленно. Эти методы следующие:
действовать | генерировать исключение | конкретное значение | блокировать | тайм-аут |
---|---|---|---|---|
вставлять | addFirst(o) | offerFirst(o) | putFirst(o) | offerFirst(o, timeout, timeunit) |
Удалить | removeFirst(o) | pollFirst(o) | takeFirst(o) | pollFirst(timeout, timeunit) |
экзамен | getFirst(o) | peekFirst(o) | никто | никто |
действовать | генерировать исключение | конкретное значение | блокировать | тайм-аут |
---|---|---|---|---|
вставлять | addLast(o) | offerLast(o) | putLast(o) | offerLast(o, timeout, timeunit) |
Удалить | removeLast(o) | pollLast(o) | takeLast(o) | pollLast(timeout, timeunit) |
экзамен | getLast(o) | peekLast(o) | никто | никто |
Объяснение четырех различных групп поведения:
- Генерация исключения: если предпринятая операция не может быть выполнена немедленно, генерируется исключение.
- конкретное значение: возвращает конкретное значение (обычно true/false), если запрошенная операция не может быть выполнена немедленно.
- Блокировка: если предпринятая операция не может быть выполнена немедленно, вызов метода будет заблокирован до тех пор, пока он не может быть выполнен.
- Тайм-аут: если предпринятая операция не может быть выполнена немедленно, вызов метода будет заблокирован до тех пор, пока ее можно будет выполнить, но не будет ждать дольше заданного значения. Возвращает конкретное значение, чтобы сообщить, была ли операция успешной (обычно true/false).
7.3 BlockingDeque наследуется от BlockingQueue
Интерфейс BlockingDeque наследуется от интерфейса BlockingQueue. Это означает, что вы можете использовать BlockingDeque как BlockingQueue. Если вы сделаете это, различные методы вставки добавят новые элементы в конец очереди, а методы удаления удалит элементы из головы очереди. Точно так же, как методы вставки и удаления интерфейса BlockingQueue.
Ниже приведена конкретная внутренняя реализация методов BlockingDeque интерфейса BlockingQueue:
BlockingQueue | BlockingDeque |
---|---|
add() | addLast() |
offer() | offerLast() |
put() | putLast() |
offer(e, time, unit) | offerLast(e, time, unit) |
remove() | removeFirst() |
poll() | pollFirst() |
take() | takeFirst() |
poll(time, unit) | pollLast(time, unit) |
element() | getFirst() |
peek() | peekFirst() |
7.4 Реализация BlockingDeque
Поскольку BlockingDeque — это интерфейс, вам придется использовать один из его многочисленных классов реализации, если вы хотите его использовать. Пакет java.util.concurrent предоставляет классы реализации для следующего интерфейса BlockingDeque: LinkedBlockingDeque.
7.5 Пример кода BlockingDeque
Вот краткий пример кода, как использовать метод BlockingDeque:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst(); 1234567
8. LinkedBlockingDeque
Класс LinkedBlockingDeque реализует интерфейс BlockingDeque.
deque — это сокращение от «Двусторонняя очередь». Итак, очередь — это очередь, в которую вы можете вставлять или извлекать элементы с любого конца.
LinkedBlockingDeque — это двусторонняя очередь, и когда она пуста, поток, пытающийся извлечь из нее данные, будет заблокирован независимо от того, с какого конца поток пытается извлечь данные.
Ниже приведен пример создания и использования LinkedBlockingDeque:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst(); 1234567
9. Параллельная карта
9.1 java.util.concurrent.ConcurrentMap
Интерфейс java.util.concurrent.ConcurrentMap представляет java.util.Map, способный к параллельной обработке доступа (вставки и извлечения) другими. ConcurrentMap имеет несколько дополнительных атомарных методов в дополнение к методам, унаследованным от его родительского интерфейса java.util.Map.
9.2 Реализация ConcurrentMap
Поскольку ConcurrentMap — это интерфейс, вы должны использовать один из его классов реализации, если хотите его использовать. В пакете java.util.concurrent есть следующие классы реализации интерфейса ConcurrentMap: ConcurrentHashMap
9.3 ConcurrentHashMap
ConcurrentHashMap похож на класс java.util.HashTable, но ConcurrentHashMap может обеспечить лучшую производительность параллелизма, чем HashTable. ConcurrentHashMap не блокирует всю карту, когда вы читаете из нее объекты. Кроме того, ConcurrentHashMap не блокирует всю карту, когда вы записываете в нее объекты. Внутри он просто блокирует часть карты, которая записывается.
Еще одно отличие состоит в том, что даже если ConcurrentHashMap модифицируется во время обхода, он не вызовет исключение ConcurrentModificationException. Хотя Iterator не предназначен для одновременного использования несколькими потоками.
Дополнительные сведения о ConcurrentMap и ConcurrentHashMap см. в официальной документации.
9.4 Пример ConcurrentMap
Ниже приведен пример использования интерфейса ConcurrentMap. В этом примере используется класс реализации ConcurrentHashMap:
ConcurrentMap concurrentMap = new ConcurrentHashMap();
concurrentMap.put("key", "value");
Object value = concurrentMap.get("key"); 12345
10. Concurrent NavigableMap
java.util.concurrent.ConcurrentNavigableMap — это java.util.NavigableMap, который поддерживает параллельный доступ, а также позволяет своим дочерним картам иметь возможности одновременного доступа. Так называемая «подкарта» относится к карте, возвращаемой такими методами, как headMap(), subMap(), tailMap().
Методы в NavigableMap повторяться не будут, в этом разделе мы рассмотрим методы, добавленные ConcurrentNavigableMap.
10.1 headMap()
Метод headMap(T toKey) возвращает подкарту, содержащую ключи меньше заданного toKey. Если вы внесете изменения в элементы исходной карты, эти изменения повлияют на элементы дочерней карты.
Следующий пример демонстрирует использование метода headMap():
ConcurrentNavigableMap map = new ConcurrentSkipListMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
ConcurrentNavigableMap headMap = map.headMap("2"); 1234567
headMap будет указывать на ConcurrentNavigableMap только с ключом «1», потому что только этот ключ меньше «2». Подробную информацию о том, как работает этот метод и его перегруженные версии, см. в документации по Java.
10.2 tailMap()
Метод tailMap(T fromKey) возвращает подкарту, содержащую ключи не меньше заданного fromKey . Если вы внесете изменения в элементы исходной карты, эти изменения повлияют на элементы дочерней карты.
Следующий пример демонстрирует использование метода tailMap():
ConcurrentNavigableMap map = new ConcurrentSkipListMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
ConcurrentNavigableMap tailMap = map.tailMap("2"); 1234567
tailMap будет иметь ключи "2" и "3", потому что они не меньше заданного ключа "2". Подробную информацию о том, как работает этот метод и его перегруженные версии, см. в документации по Java.
10.3 subMap()
Метод subMap() возвращает подкарту исходной карты с ключами от (включительно) до (исключительно). Пример выглядит следующим образом:
ConcurrentNavigableMap map = new ConcurrentSkipListMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
ConcurrentNavigableMap subMap = map.subMap("2", "3"); 1234567
Возвращаемая подкарта содержит только ключ "2", потому что только он удовлетворяет не менее "2", что меньше "3".
10.4 Дополнительные методы
Интерфейс ConcurrentNavigableMap имеет другие доступные методы, такие как:
- descendingKeySet()
- descendingMap()
- navigableKeySet()
Обратитесь к официальной документации Java для получения дополнительной информации об этих методах.
11. Защелка CountDownLatch
java.util.concurrent.CountDownLatch — это параллельная конструкция, которая позволяет одному или нескольким потокам ожидать завершения последовательности указанных операций. CountDownLatch инициализируется заданным числом. Этот счетчик уменьшается на единицу каждый раз, когда вызывается countDown(). Вызывая один из методов await(), поток может блокироваться до тех пор, пока это число не достигнет нуля.
Ниже приведен простой пример. Ожидающий ожидающий не будет освобожден от вызова await() до тех пор, пока декрементатор не вызовет countDown() три раза.
CountDownLatch latch = new CountDownLatch(3);
Waiter waiter = new Waiter(latch);
Decrementer decrementer = new Decrementer(latch);
new Thread(waiter).start();
new Thread(decrementer).start();
Thread.sleep(4000);
public class Waiter implements Runnable{
CountDownLatch latch = null;
public Waiter(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Waiter Released");
}
}
public class Decrementer implements Runnable {
CountDownLatch latch = null;
public Decrementer(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
Thread.sleep(1000);
this.latch.countDown();
Thread.sleep(1000);
this.latch.countDown();
Thread.sleep(1000);
this.latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
12. Циклический барьер
Класс java.util.concurrent.CyclicBarrier представляет собой механизм синхронизации, который синхронизирует потоки, обрабатывающие некоторые алгоритмы. Другими словами, это забор, в котором все потоки должны ждать, пока все потоки не доберутся сюда, а затем все потоки могут перейти к другим вещам. Схема выглядит следующим образом:
Два потока ждут друг друга у забора. Два потока могут ждать друг друга, вызывая метод await() объекта CyclicBarrier. Как только N потоков ожидают достижения CyclicBarrier, все потоки будут освобождены для продолжения работы.
12.1 Создание циклического барьера
При создании CyclicBarrier вам необходимо определить, сколько потоков будет ожидать на барьере, прежде чем будет освобождено. Создайте пример CyclicBarrier:
CyclicBarrier barrier = new CyclicBarrier(2); 1
12.2 Ожидание CyclicBarrier
Ниже показано, как заставить поток ожидать CyclicBarrier:
barrier.await(); 1
Конечно, вы также можете установить тайм-аут для ожидающих потоков. После ожидания, превышающего период тайм-аута, поток будет освобожден, даже если условие для N потоков ожидания CyclicBarrier не было достигнуто. Ниже приведен пример определения периода ожидания:
barrier.await(10, TimeUnit.SECONDS); 1
Поток, ожидающий CyclicBarrier, может быть освобожден, если выполняется одно из следующих условий:
- Последний поток также достигает CyclicBarrier (вызывает await())
- Текущий поток прерывается другими потоками (другие потоки вызывают метод прерывания() потока)
- Другие потоки, ожидающие забора, прерываются
- Другие потоки, ожидающие забора, были освобождены из-за тайм-аута.
- Метод CyclicBarrier.reset() барьера был вызван внешним потоком
12.3 Действие циклического барьера
CyclicBarrier поддерживает действие ограждения, которое представляет собой экземпляр Runnable, который будет выполняться, как только прибудет последний поток, ожидающий ограждения. Вы можете передать действие барьера Runnable в CyclicBarrier в его конструкторе:
Runnable barrierAction = ... ;
CyclicBarrier barrier = new CyclicBarrier(2, barrierAction); 12
12.4 Пример циклического барьера
Следующий код демонстрирует, как использовать CyclicBarrier:
Runnable barrier1Action = new Runnable() {
public void run() {
System.out.println("BarrierAction 1 executed ");
}
};
Runnable barrier2Action = new Runnable() {
public void run() {
System.out.println("BarrierAction 2 executed ");
}
};
CyclicBarrier barrier1 = new CyclicBarrier(2, barrier1Action);
CyclicBarrier barrier2 = new CyclicBarrier(2, barrier2Action);
CyclicBarrierRunnable barrierRunnable1 = new CyclicBarrierRunnable(barrier1, barrier2);
CyclicBarrierRunnable barrierRunnable2 = new CyclicBarrierRunnable(barrier1, barrier2);
new Thread(barrierRunnable1).start();
new Thread(barrierRunnable2).start(); 1234567891011121314151617181920
Класс CyclicBarrierRunnable:
public class CyclicBarrierRunnable implements Runnable{
CyclicBarrier barrier1 = null;
CyclicBarrier barrier2 = null;
public CyclicBarrierRunnable(
CyclicBarrier barrier1,
CyclicBarrier barrier2) {
this.barrier1 = barrier1;
this.barrier2 = barrier2;
}
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" waiting at barrier 1");
this.barrier1.await();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" waiting at barrier 2");
this.barrier2.await();
System.out.println(Thread.currentThread().getName() +
" done!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
} 1234567891011121314151617181920212223242526272829303132333435
Консольный вывод приведенного выше кода выглядит следующим образом. Обратите внимание, что время записи каждого потока на консоль может отличаться от фактического выполнения. Например, иногда сначала печатается Thread-0, а иногда сначала печатается Thread-1.
Thread-0 waiting at barrier 1
Thread-1 waiting at barrier 1
BarrierAction 1 executed
Thread-1 waiting at barrier 2
Thread-0 waiting at barrier 2
BarrierAction 2 executed
Thread-0 done!
Thread-1 done!12345678
13. Обменник
Класс java.util.concurrent.Exchanger представляет собой точку встречи, где два потока могут обмениваться объектами друг с другом. Этот механизм иллюстрируется следующим образом:
Два потока обмениваются объектами через Exchanger.
Действие по обмену объектами выполняется одним из двух методов exchange() Exchange. Вот пример:
Exchanger exchanger = new Exchanger();
ExchangerRunnable exchangerRunnable1 =
new ExchangerRunnable(exchanger, "A");
ExchangerRunnable exchangerRunnable2 =
new ExchangerRunnable(exchanger, "B");
new Thread(exchangerRunnable1).start();
new Thread(exchangerRunnable2).start();
ExchangerRunnable 代码:
```java
public class ExchangerRunnable implements Runnable{
Exchanger exchanger = null;
Object object = null;
public ExchangerRunnable(Exchanger exchanger, Object object) {
this.exchanger = exchanger;
this.object = object;
}
public void run() {
try {
Object previous = this.object;
this.object = this.exchanger.exchange(this.object);
System.out.println(
Thread.currentThread().getName() +
" exchanged " + previous + " for " + this.object
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} 12345678910111213141516171819202122232425262728293031323334353637
Вывод вышеуказанной программы:
Thread-0 exchanged A for B
Thread-1 exchanged B for A12
14. Семафор
Класс java.util.concurrent.Semaphore представляет собой счетный семафор. Это означает, что у него есть два основных метода:
acquire()
release()12
Счетный семафор инициализируется с определенным количеством «разрешений». Каждый раз, когда вызывается методAcquire(), вызывающий поток получает разрешение. При каждом вызове release() семафору возвращается лицензия. Таким образом, без каких-либо вызовов release() не более N потоков могут пройти через методAcquire(), где N — указанное количество разрешений при инициализации семафора. Эти лицензии просто счетчик. Здесь нет ничего необычного.
14.1 Использование семафоров
Есть два основных назначения семафоров:
- Защитите критическую секцию (кода) от входа в более чем N потоков одновременно
- Отправить сигнал между двумя потоками
14.2 Защитите важные детали
Если вы используете семафор для защиты важного раздела, код, который пытается войти в этот раздел, обычно сначала пытается получить разрешение перед входом в важный раздел (блок кода), а затем освобождает разрешение после завершения выполнения. Например:
Semaphore semaphore = new Semaphore(1);
//critical section
semaphore.acquire();
...
semaphore.release(); 12345678
14.3 Отправка сигналов между потоками
Если вы используете семафор для передачи сигналов между двумя потоками, вы должны обычно вызывать методAcquire() в одном потоке и Release() в другом потоке.
Если лицензии недоступны, вызовacquire() будет заблокирован до тех пор, пока лицензия не будет освобождена другим потоком. Аналогичным образом, вызов release() будет заблокирован, если для семафора больше не будет разрешений.
Благодаря этому можно координировать несколько потоков. Например, если поток 1 вызывает метод accept() после вставки объекта в общий список, а поток 2 вызывает метод release() перед получением объекта из списка, вы создали очередь блокировки. Количество лицензий, доступных в семафоре, равно количеству элементов, которые может содержать очередь блокировки.
14.4 Справедливость
Нет никакого способа гарантировать, что потокам может быть предоставлено разрешение от семафора справедливо. То есть нет никакой гарантии, что поток, первым вызвавший методAcquire(), первым получит разрешение. Если первый поток блокируется в ожидании разрешения, а второй поток приходит запрашивать разрешение как раз в тот момент, когда разрешение освобождается, он может получить разрешение раньше первого потока. Если вы хотите обеспечить справедливость, класс Semaphore имеет конструктор, который принимает логический параметр, который сообщает Semaphore, следует ли применять справедливость. Принудительная справедливость влияет на производительность параллелизма, поэтому не включайте ее, если она вам действительно не нужна.
Вот пример того, как создать семафор в честном режиме:
Semaphore semaphore = new Semaphore(1, true); 1
14.5 Дополнительные методы
Класс java.util.concurrent.Semaphore также имеет множество методов, таких как:
- availablePermits()
- acquireUninterruptibly()
- drainPermits()
- hasQueuedThreads()
- getQueuedThreads()
- tryAcquire()
Подробную информацию об этих методах см. в документации по Java.
15. Служба исполнителя ExecutorService
Интерфейс java.util.concurrent.ExecutorService представляет собой механизм асинхронного выполнения, который позволяет нам выполнять задачи в фоновом режиме. Таким образом, ExecutorService очень похож на пул потоков. На самом деле реализация ExecutorService, существующая в пакете java.util.concurrent, является реализацией пула потоков.
15.1 Пример ExecutorService
Вот простой пример ExecutorService:
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
executorService.shutdown(); 123456789
Сначала создайте ExecutorService, используя фабричный метод newFixedThreadPool(). Здесь создается пул из десяти потоков для выполнения задач. Затем передайте анонимную реализацию интерфейса Runnable методу execute(). Это заставит поток в ExecutorService выполнить Runnable.
15.2 Делегирование задач
На следующей диаграмме показано, как поток делегирует задачу ExecutorService для асинхронного выполнения.
Поток делегирует задачу ExecutorService для асинхронного выполнения. Как только поток делегирует задачу ExecutorService, поток продолжит свое собственное выполнение независимо от выполнения задачи.
15.3 Реализация ExecutorService
Поскольку ExecutorService — это интерфейс, если вы хотите его использовать, вам нужно использовать один из его классов реализации. Пакет java.util.concurrent предоставляет следующие классы реализации интерфейса ExecutorService:
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
15.4 Создание ExecutorService
Создание ExecutorService зависит от конкретной реализации, которую вы используете. Но вы также можете использовать фабричный класс Executors для создания экземпляров ExecutorService. Вот несколько примеров создания экземпляров ExecutorService:
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newScheduledThreadPool(10); 123
15.5 Использование ExecutorService
Существует несколько различных способов делегирования задач ExecutorService для выполнения:
- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- вызыватьЛюбой(…)
- вызватьВсе(…)
Давайте рассмотрим эти методы один за другим.
15.6 execute(Runnable)
Метод execute(Runnable) требует объект java.lang.Runnable и выполняет его асинхронно. Вот пример выполнения Runnable с помощью ExecutorService:
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
executorService.shutdown(); 123456789
Невозможно узнать результат выполнения исполняемого Runnable. При необходимости вы должны использовать Callable (описано ниже).
15.7 submit(Runnable)
Для метода submit(Runnable) также требуется класс реализации Runnable, но он возвращает объект Future. Этот объект Future можно использовать для проверки завершения выполнения Runnable.
Вот пример ExecutorService submit():
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
future.get(); //returns null if the task has finished correctly. 1234567
15.8 submit(Callable)
Метод submit(Callable) аналогичен методу submit(Runnable), за исключением требуемых типов параметров. Экземпляр Callable очень похож на Runnable, за исключением того, что его метод call() может возвращать результат. Runnable.run() не может вернуть результат. Результат Callable можно получить через объект Future, возвращаемый методом submit(Callable). Вот пример вызываемого ExecutorService:
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get()); 12345678
Приведенный выше код выводит:
Asynchronous Callable
future.get() = Callable Result12
15.9 invokeAny()
Для метода invokeAny() требуется массив объектов экземпляров Callable или его подынтерфейса. Вызов этого метода не возвращает Future, но возвращает результат одного из объектов Callable. Нет никакой гарантии, какой результат Callable будет возвращен — только один из них завершил выполнение.
Если одна из задач завершится (или выдаст исключение), другие Callables будут отменены.
Вот пример кода:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown(); 12345678910111213141516171819202122232425
Приведенный выше код напечатает результат выполнения одной из заданных коллекций Callable. Я пробовал это несколько раз сам, и результаты всегда меняются. Иногда «Задание 1», иногда «Задание 2» и так далее.
15.10 invokeAll()
Метод invokeAll() будет вызывать все объекты Callable, которые вы передаете в ExecutorService в коллекции. invokeAll() возвращает ряд объектов Future, с помощью которых вы можете получить результат выполнения каждого Callable.
Помните, что задача может закончиться исключением, поэтому она может не «успешно выполниться». Невозможно сказать нам, какое из двух окончаний мы имеем с объектом Future.
Вот пример кода:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown(); 123456789101112131415161718192021222324252627
15.11 Завершение работы ExecutorService
После использования ExecutorService вы должны закрыть его, чтобы потоки в нем больше не выполнялись.
Например, если ваше приложение запускается с помощью метода main(), а затем основной метод выходит из вашего приложения, он будет продолжать работать, если ваше приложение имеет активный ExexutorService. Активные потоки в ExecutorService предотвращают завершение работы JVM.
Чтобы завершить потоки в ExecutorService, вам нужно вызвать метод shutdown() ExecutorService. Служба ExecutorService не будет закрыта немедленно, но больше не будет принимать новые задачи, и как только все потоки завершат текущую задачу, служба ExecutorService будет закрыта. Все задачи, отправленные в ExecutorService, выполняются до вызова shutdown().
Если вы хотите немедленно закрыть ExecutorService, вы можете вызвать метод shutdownNow(). Это немедленно пытается остановить все выполняемые задачи и игнорирует те, которые были отправлены, но еще не начали обработку. Правильное выполнение выполняемых задач не может быть гарантировано. Может быть, их остановили, а может, они уже закончили выполнение.
16. Возвращаемый поток: Java Callable
Интерфейс Java Callable java.util.concurrent.Callable представляет собой асинхронную задачу, которая может выполняться отдельным потоком. Например, вызываемый объект может быть отправлен в Java ExecutorService, который затем выполнит его асинхронно.
16.1 Определение вызываемого интерфейса
Интерфейс Java Callable очень прост. Он содержит метод call(). Ниже приведен вызываемый интерфейс:
public interface Callable<V> {
V call() throws Exception;
}
Вызовите метод call() для выполнения асинхронной задачи. Метод call() может возвращать результат. Если задача выполняется асинхронно, результат обычно передается создателю задачи через Java Future. Когда Callable отправляется в ExecutorService для одновременного выполнения, объект Future может использоваться для получения возвращаемого результата.
Метод call() также может генерировать исключение, если задача завершается сбоем во время выполнения.
16.2 Реализация интерфейса
Вот простой пример реализации интерфейса Java Callable:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return String.valueOf(System.currentTimeMillis());
}
}
Эта реализация очень проста. В результате метод call() вернет строку текущего времени. В реальном приложении задача может быть более сложной или большим набором операций.
Как правило, операции ввода-вывода, такие как чтение или запись на диск или в сеть, являются задачами, которые могут выполняться одновременно. Операции ввода-вывода часто имеют длительное время ожидания между чтением и записью блоков данных. Выполняя такие задачи в отдельном потоке, вы можете избежать ненужной блокировки основного потока.
16.3 Вызываемый и работающий
Интерфейс Java Callable подобен интерфейсу Java Runnable в том, что они оба представляют задачи, которые могут выполняться одновременно отдельными потоками.
Java Callable отличается от Runnable тем, что метод run() интерфейса Runnable не возвращает значение и не может генерировать проверенные исключения (только RuntimeException ).
Кроме того, Runnable изначально был разработан для длительных параллельных задач, таких как одновременная работа веб-серверов или просмотр каталогов для поиска новых файлов. Интерфейс Callable больше подходит для одноразовых задач, которые возвращают один результат.
17 Результат выполнения потока: будущее Java
Java Future, java.util.concurrent.Future представляет собой результат асинхронных вычислений. Объект Java, возвращающий Future в виде потока при создании асинхронной задачи. Этот объект Future используется как дескриптор результата асинхронной задачи. После завершения асинхронной задачи доступ к результату можно получить через объект Future.
Некоторые встроенные в Java службы параллелизма (например, Java ExecutorService) возвращают объекты Future из некоторых своих методов. В этом случае ExecutorService вернет объект Future при отправке и выполнении задачи Callable.
17.1 Определение интерфейса
Чтобы понять, как работает интерфейс Java Future, определение интерфейса:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning)
V get();
V get(long timeout, TimeUnit unit);
boolean isCancelled();
boolean isDone();
}
Каждый из этих методов будет рассмотрен в последующих главах, но, как вы видите, интерфейс Java Future не так уж высокоуровнев.
17.2 Получение результатов от фьючерсов
Как упоминалось ранее, Java Future представляет собой результат асинхронной задачи. Чтобы получить результат, вызовите один из двух методов get() объекта Future. Все методы get() возвращают объект, но тип возвращаемого значения также может быть общим типом возвращаемого значения (ссылающимся на объект определенного класса, а не только на объект). Вот пример Future, получающий результат через свой метод get():
Future future = ... // get Future by starting async task
// do something else, until ready to check result via Future
// get result from Future
try {
Object result = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Если метод get() вызывается до завершения асинхронной задачи, метод get() будет заблокирован до завершения выполнения потока.
Второй метод get() может вернуться с тайм-аутом через определенный период времени, вы можете указать период тайм-аута через параметр метода. Вот пример вызова этой версии get():
try {
Object result =
future.get(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
} catch (ExecutionException e) {
} catch (TimeoutException e) {
// thrown if timeout time interval passes
// before a result is available.
}
В приведенном выше примере Future ожидает до 1000 миллисекунд. Выдает TimeoutException, если в течение 1000 миллисекунд нет доступных результатов.
17.3 Отмена задач через Future
Вы можете отменить соответствующую асинхронную задачу, вызвав метод Cancel() класса Future. Асинхронная задача должна быть реализована и выполняться. В противном случае вызов cancel() не будет иметь никакого эффекта. Следующее через Java FutureПример метода CANCEN () для отмены задачи:
future.cancel();
17.4 Проверка выполнения задачи
Вы можете проверить, завершена ли асинхронная задача (и доступен ли результат), вызвав метод Future isDone(). Ниже приведен пример вызова метода Java Future isDone():
Future future = ... // Get Future from somewhere
if(future.isDone()) {
Object result = future.get();
} else {
// do something else
}
17.5 Проверка отмены задачи
Также можно проверить, отменена ли асинхронная задача, представленная Java. Вы можете сделать это, позвонив в FutureisCancelled() для проверки. Вот пример проверки отмены задачи:
Future future = ... // get Future from somewhere
if(future.isCancelled()) {
} else {
}
18. ThreadPoolExecutor
java.util.concurrent.ThreadPoolExecutor — это реализация интерфейса ExecutorService. ThreadPoolExecutor выполняет заданную задачу (Callable или Runnable), используя потоки из своего внутреннего пула.
ThreadPoolExecutor содержит пулы потоков, которые могут содержать разное количество потоков. Количество потоков в пуле определяется следующими переменными:
- corePoolSize
- maximumPoolSize
Когда задача делегируется в пул потоков, если количество потоков в пуле падает ниже corePoolSize, будет создан новый поток, даже если в пуле могут быть простаивающие потоки. Если внутренняя очередь задач заполнена и выполняется хотя бы corePoolSize, но количество запущенных потоков меньше maxPoolSize, для выполнения задачи будет создан новый поток. Диаграмма ThreadPoolExecutor:
ThreadPoolExecutor
18.1 Создание ThreadPoolExecutor
ThreadPoolExecutor имеет несколько доступных конструкторов. Например:
int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 5000;
ExecutorService threadPoolExecutor =
new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
); 123456789101112
Однако, если вам действительно не нужно явно определять все параметры для ThreadPoolExecutor, удобнее использовать один из фабричных методов в классе java.util.concurrent.Executors, как описано в подразделе ExecutorService.
19. Служба запланированного исполнителя ScheduledExecutorService
java.util.concurrent.ScheduledExecutorService — это ExecutorService, который может задерживать выполнение задач или выполнять их несколько раз с фиксированным интервалом времени. Задачи выполняются асинхронно рабочим потоком, а не потоком, отправившим задачу в ScheduledExecutorService.
19.1 Пример службы ScheduledExecutorService
Вот простой пример ScheduledExecutorService:
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
},
5,
TimeUnit.SECONDS); 123456789101112
Сначала создается ScheduledExecutorService с 5 встроенными потоками. Затем создается анонимный экземпляр класса интерфейса Callable, который передается методу schedule(). Последние два параметра определяют, что Callable будет выполнен через 5 секунд.
19.2 Реализация ScheduledExecutorService
Так как ScheduledExecutorService — это интерфейс, если вы хотите его использовать, вы должны использовать один из его классов реализации в пакете java.util.concurrent. ScheduledExecutorService имеет следующий класс реализации: ScheduledThreadPoolExecutor.
19.3 Создание службы ScheduledExecutorService
Способ создания ScheduledExecutorService зависит от используемого класса реализации. Но вы также можете использовать фабричный класс Executors для создания экземпляра ScheduledExecutorService. Например:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); 1
19.4 Использование ScheduledExecutorService
Создав ScheduledExecutorService, вы можете вызвать его, вызвав следующие методы:
- schedule (Callable task, long delay, TimeUnit timeunit)
- schedule (Runnable task, long delay, TimeUnit timeunit)
- scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
- scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
Ниже мы кратко рассмотрим эти методы.
schedule (Callable task, long delay, TimeUnit timeunit)1
Этот метод планирует выполнение указанного Callable после заданной задержки. Этот метод возвращает ScheduledFuture, с помощью которого вы можете отменить его до его выполнения или получить результат после его выполнения. Вот пример:
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
},
5,
TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown(); 12345678910111213141516
Пример вывода:
Executed!
result = Called!
123
schedule (Runnable task, long delay, TimeUnit timeunit)1
Этот метод работает точно так же, как версия, которая принимает Callable в качестве параметра, за исключением того, что Runnable не может возвращать результат, поэтому ScheduledFuture.get() возвращает null после завершения выполнения задачи.
scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)1
Этот метод планирует выполнение задачи на регулярной основе. Задача будет выполняться после первой начальной задержки, а затем повторяться после каждого периода.
Если выполнение данной задачи вызывает исключение, задача больше не будет выполняться. Если исключений нет, задача будет продолжать выполняться до тех пор, пока не будет закрыта служба ScheduledExecutorService. Если задача выполняется дольше запланированного интервала времени, следующее выполнение не начнется, пока не завершится текущее выполнение. Запланированные задачи не имеют нескольких потоков, выполняющихся одновременно.
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)1
Этот метод очень похож на scheduleAtFixedRate(), за исключением того, что период интерпретируется иначе.
В методе scheduleAtFixedRate() период интерпретируется как интервал времени между началом предыдущего выполнения и началом следующего выполнения.
В этом методе период интерпретируется как интервал между окончанием предыдущего выполнения и окончанием следующего выполнения. Таким образом, эта задержка является интервалом между окончанием выполнения, а не интервалом между началом выполнения.
19.5 Завершение работы ScheduledExecutorService
Как и в случае с ExecutorService, вам необходимо закрыть ScheduledExecutorService после завершения его использования. В противном случае это приведет к тому, что JVM продолжит работу, даже если все остальные потоки были закрыты.
Вы можете закрыть ScheduledExecutorService с помощью методов shutdown() или shutdownNow(), унаследованных от интерфейса ExecutorService. Дополнительную информацию см. в разделе завершения работы ExecutorService.
20. Форк и слияние с помощью ForkJoinPool
ForkJoinPool был представлен в Java 7. Он похож на ExecutorService с одним отличием. ForkJoinPool позволяет нам легко разделить задачи на несколько более мелких задач, и эти разделенные задачи также будут отправлены в ForkJoinPool. Задачу можно по-прежнему разбивать на более мелкие подзадачи до тех пор, пока она еще может быть разделена. Это может показаться абстрактным, поэтому в этом разделе мы объясним, как работает ForkJoinPool и как работает разделение задач.
20.1 Объяснение форков и слияний
Прежде чем мы начнем рассматривать ForkJoinPool, давайте кратко объясним, как работают форки и слияния. Принцип fork and merge состоит из двух рекурсивно выполняемых шагов. Два шага — это шаг разветвления и шаг слияния.
20.2 Вилки
Задача, использующая принцип разветвления и слияния, может разветвляться (разделяться) на более мелкие подзадачи, которые могут выполняться одновременно. Как показано ниже:
Разделяя себя на несколько подзадач, каждая подзадача может выполняться параллельно разными процессорами или разными потоками на одном и том же процессоре.
Разбивать ее на несколько подзадач имеет смысл только тогда, когда данная задача слишком велика. Разделение задачи на подзадачи сопряжено с определенными накладными расходами, поэтому для небольших задач стоимость такого разделения может быть больше, чем стоимость одновременного выполнения каждой подзадачи.
Когда имеет смысл разбить задачу на подзадачи, этот предел также называется порогом. Это зависит от решения каждой задачи на значимом пороге. Многое зависит от того, какую работу он выполняет.
20.3 Слияние
Когда задача разбивается на несколько подзадач, задача переходит в ожидание окончания всех подзадач.
После завершения выполнения подзадачи задача может объединить все результаты в один результат. Схема выглядит следующим образом:
Конечно, не все типы задач возвращают результат. Если задача не возвращает результат, она просто ждет завершения выполнения всех подзадач. Нет необходимости объединять результаты.
20.4 ForkJoinPool
ForkJoinPool — это специальный пул потоков, разработанный для лучшей работы с разделением задач по принципу «разветвление и соединение». ForkJoinPool также находится в пакете java.util.concurrent, а его полное имя класса — java.util.concurrent.ForkJoinPool.
20.5 Создание пула ForkJoinPool
Вы можете создать ForkJoinPool через его конструктор. В качестве аргумента, передаваемого конструктору ForkJoinPool, вы можете определить ожидаемый уровень параллелизма. Уровень параллелизма представляет собой количество потоков или ЦП, необходимых для задачи, которую вы хотите передать в ForkJoinPool. Вот пример ForkJoinPool:
ForkJoinPool forkJoinPool = new ForkJoinPool(4); 1
В этом примере создается ForkJoinPool с уровнем параллелизма 4.
20.6 Отправка задач в ForkJoinPool
Отправляйте задачи в ForkJoinPool так же, как отправляйте задачи в ExecutorService. Вы можете отправить два типа задач. Один без возвращаемого значения («действие»), другой с возвращаемым значением («задача»). Эти два типа представлены RecursiveAction и RecursiveTask соответственно. В следующих разделах описано, как использовать эти два типа задач и как их отправлять.
20.7 RecursiveAction
RecursiveAction — это задача, не имеющая возвращаемого значения. Он просто выполняет какую-то работу, например записывает данные на диск, а затем завершает работу. RecursiveAction может разделить свою работу на более мелкие части, чтобы они могли выполняться отдельными потоками или процессорами. Вы можете реализовать RecursiveAction через наследование. Пример выглядит следующим образом:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List<MyRecursiveAction> createSubtasks() {
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
Пример простой. MyRecursiveAction передает воображаемую рабочую нагрузку в качестве параметра своему конструктору. Если рабочая нагрузка превышает определенный порог, работа будет разделена на несколько подзаданий, и подзадания продолжат разделяться. Если workLoad упадет ниже определенного порога, работу будет выполнять сам MyRecursiveAction.
Вы можете запланировать выполнение MyRecursiveAction следующим образом:
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
forkJoinPool.invoke(myRecursiveAction); 123
20.8 RecursiveTask
RecursiveTask — это задача, которая возвращает результат. Он может разделить свою работу на несколько более мелких задач и объединить результаты выполнения этих подзадач в коллективный результат. Горизонтальных разделений и слияний может быть несколько. Ниже приведен пример RecursiveTask:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<Long> {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
subtasks.addAll(createSubtasks());
for(MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List<MyRecursiveTask> createSubtasks() {
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
} 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
Этот пример очень похож на пример RecursiveAction, за исключением того, что возвращается результат. Класс MyRecursiveTask наследуется от RecursiveTask, что означает, что он будет возвращать результат типа Long.
Пример MyRecursiveTask также разбивает работу на подзадачи и планирует выполнение этих подзадач с помощью метода fork().
Кроме того, в этом примере собираются результаты, возвращаемые каждой подзадачей, путем вызова их метода join(). Затем результаты подзадач объединяются в более крупный результат, который в конечном итоге возвращается. Для разных уровней рекурсии рекурсия может возникнуть в результате этого слияния подзадач.
Вы можете спланировать RecursiveTask следующим образом:
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult); 12345
Обратите внимание, как окончательный результат выполнения получается при вызове метода ForkJoinPool.invoke().
20.9 Комментарии ForkJoinPool
Похоже, не все довольны ForkJoinPool в Java 7:«Катастрофа форк-слияния Java». Рекомендуется прочитать эту статью, прежде чем вы планируете использовать ForkJoinPool в своих проектах.
21. Замок
java.util.concurrent.locks.Lock — это механизм синхронизации потоков, аналогичный синхронизированным блокам. Но блокировка более гибкая и детальная, чем синхронизированный блок. Кстати, в моемРуководство по параллелизму JavaВ я описал, как реализовать собственную блокировку.
21.1 Пример блокировки Java
Поскольку Lock — это интерфейс, вам нужно использовать один из его классов реализации, чтобы использовать его в своей программе. Вот простой пример:
Lock lock = new ReentrantLock();
lock.lock();
//critical section
lock.unlock(); 1234567
Сначала создается объект блокировки. Затем вызывается его метод lock(). В это время экземпляр блокировки заблокирован. Любой другой поток, вызывающий lock(), будет заблокирован до тех пор, пока поток, заблокировавший экземпляр блокировки, не вызовет unlock(). Наконец, вызывается функция unlock(), объект блокировки разблокируется, и другие потоки могут его заблокировать.
21.2 Реализация блокировки Java
Пакет java.util.concurrent.locks предоставляет следующие классы реализации для интерфейса Lock: ReentrantLock
21.3 Основное различие между Lock и синхронизированными блоками
Основные различия между объектом блокировки и синхронизированным блоком:
- Синхронизированный блок кода не гарантирует порядок, в котором вводятся потоки, входящие в ожидание доступа.
- Вы не можете передавать какие-либо параметры в запись синхронизированного блока. Поэтому невозможно установить тайм-аут для ожидания доступа к синхронизированным блокам.
- Синхронизированный блок должен полностью содержаться в одном методе. И объект Lock может поместить свои вызовы методов lock() и unlock() в разные методы.
21.4 Метод блокировки
Интерфейс Lock имеет следующие основные методы:
- lock()
lock() блокирует экземпляр Lock. Если экземпляр Lock уже заблокирован, поток, вызывающий метод lock(), будет заблокирован до тех пор, пока экземпляр Lock не будет разблокирован.
- lockInterruptibly()
Метод lockInterruptably() будет заблокирован вызывающим потоком, если поток не будет прерван. Кроме того, если поток входит в ожидание блокировки при блокировке объекта Lock с помощью этого метода и прерывается, поток завершает вызов этого метода.
- tryLock()
Метод tryLock() пытается немедленно заблокировать экземпляр Lock. Он вернет true, если блокировка прошла успешно, и false, если экземпляр Lock был заблокирован. Этот метод никогда не блокирует.
- tryLock(long timeout, TimeUnit timeUnit)
tryLock(long timeout, TimeUnit timeUnit) работает так же, как и метод tryLock(), за исключением того, что он ждет заданный тайм-аут, прежде чем снять блокировку Lock.
- unlock()
Метод unlock() разблокирует экземпляр Lock. Реализация Lock позволит вызывать этот метод только тем потокам, которые заблокировали объект. Вызовы метода unlock() другими потоками (потоками, которые не заблокировали объект Lock) вызовут непроверенное исключение (RuntimeException).
22. ReadWriteLock
Блокировка чтения-записи java.util.concurrent.locks.ReadWriteLock — это расширенный механизм блокировки потоков. Он позволяет нескольким потокам читать определенный ресурс одновременно, но только один поток может записывать в него одновременно.
Идея блокировки чтения-записи заключается в том, что несколько потоков могут читать общий ресурс, не вызывая проблем параллелизма. Проблема параллелизма возникает, когда операции чтения и записи общего ресурса выполняются одновременно или одновременно выполняются несколько операций записи.
В этом разделе обсуждается только встроенный в Java ReadWriteLock. Если вы хотите понять принцип реализации ReadWriteLock, обратитесь к моемуРуководство по параллелизму JavaПодраздел "Блокировки чтения-записи" в теме.
22.1 Правила блокировки ReadWriteLock
Правила блокировки ReadWriteLock потоком перед чтением или записью защищенного ресурса следующие: Блокировка чтения: если ни один поток записи не заблокировал ReadWriteLock, и ни один поток записи не запросил блокировку записи (но еще не получил блокировку). Следовательно, несколько потоков чтения могут блокировать блокировку.
Блокировка записи: если нет операций чтения или записи. Поэтому во время операции записи только один поток может заблокировать блокировку.
22.2 Реализация ReadWriteLock
ReadWriteLock — это интерфейс, и если вы хотите его использовать, вам нужно использовать один из его классов реализации. Пакет java.util.concurrent.locks предоставляет следующие классы реализации интерфейса ReadWriteLock: ReentrantReadWriteLock.
22.3 Пример кода ReadWriteLock
Ниже приведен простой пример кода для создания ReadWriteLock и его использования для блокировки чтения и записи:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
// multiple readers can enter this section
// if not locked for writing, and not writers waiting
// to lock for writing.
readWriteLock.readLock().unlock();
readWriteLock.writeLock().lock();
// only one writer can enter this section,
// and only if no threads are currently reading.
readWriteLock.writeLock().unlock(); 12345678910111213141516
Обратите внимание, как ReadWriteLock используется для удержания обоих экземпляров блокировки. Один защищает доступ для чтения, а другой защищает доступ для записи.
23. Атомное логическое значение
Класс AtomicBoolean предоставляет нам логическое значение, которое можно читать и записывать атомарно, а также он имеет некоторые расширенные атомарные операции, такие как compareAndSet(). Класс AtomicBoolean находится в пакете java.util.concurrent.atomic, а полное имя класса — java.util.concurrent.atomic.AtomicBoolean. AtomicBoolean, описанный в этом разделе, относится к версии Java 8, а не к версии Java 5, где он был впервые представлен.
Философия дизайна, лежащая в основе AtomicBoolean, находится в моем понимании.Руководство по параллелизму JavaТема«Сравни и обменяй»Разделы имеют пояснения.
23.1 Создание AtomicBoolean
Вы можете создать AtomicBoolean следующим образом:
AtomicBoolean atomicBoolean = new AtomicBoolean(); 1
В приведенном выше примере создается новый AtomicBoolean со значением по умолчанию false.
Если вы хотите установить явное начальное значение для экземпляра AtomicBoolean, вы можете передать начальное значение конструктору AtomicBoolean:
AtomicBoolean atomicBoolean = new AtomicBoolean(true); 1
23.2 Получить значение AtomicBoolean
Вы можете получить значение AtomicBoolean, используя метод get(). Пример выглядит следующим образом:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get(); 123
Значение переменной value будет равно true после выполнения приведенного выше кода.
23.3 Установка значения AtomicBoolean
Вы можете установить значение AtomicBoolean с помощью метода set(). Пример выглядит следующим образом:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false); 123
После выполнения приведенного выше кода значение AtomicBoolean становится ложным.
23.4 Обмен значениями AtomicBoolean
Вы можете обменять значение экземпляра AtomicBoolean с помощью метода getAndSet(). Метод getAndSet() вернет текущее значение AtomicBoolean и установит новое значение для AtomicBoolean. Пример выглядит следующим образом:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false); 12
После выполнения приведенного выше кода значение переменной oldValue равно true, а экземпляр atomicBoolean будет содержать значение false. Код успешно заменяет текущее значение AtomicBoolean true на false.
23.5 Сравнение и установка значения AtomicBoolean
Метод compareAndSet() позволяет сравнить текущее значение AtomicBoolean с ожидаемым значением и установить новое значение для AtomicBoolean, если текущее значение равно ожидаемому значению. Метод compareAndSet() является атомарным, поэтому его одновременно выполняет один поток. Поэтому метод compareAndSet() можно использовать для некоторых простых реализаций блокировки, подобной синхронизации.
Вот пример сравненияAndSet():
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean expectedValue = true;
boolean newValue = false;
boolean wasNewValueSet = atomicBoolean.compareAndSet(expectedValue, newValue); 123456
В этом примере текущее значение AtomicBoolean сравнивается со значением true и, если оно равно, обновляет значение AtomicBoolean до false.
24. Атомное целое
Класс AtomicInteger предоставляет нам переменную типа int, которую можно читать и записывать атомарно, а также содержит ряд расширенных атомарных операций, таких как compareAndSet(). Класс AtomicInteger находится в пакете java.util.concurrent.atomic, поэтому его полное имя класса — java.util.concurrent.atomic.AtomicInteger. AtomicInteger, описанный в этом разделе, относится к версии Java 8, а не к версии Java 5, в которой он был впервые представлен.
Философия дизайна, лежащая в основе AtomicInteger, находится в моем понимании.Руководство по параллелизму JavaТема«Сравни и обменяй»Разделы имеют пояснения.
24.1 Создание AtomicInteger
Пример создания AtomicInteger выглядит следующим образом:
AtomicInteger atomicInteger = new AtomicInteger(); 1
В этом примере создается AtomicInteger с начальным значением 0. Если вы хотите создать AtomicInteger с заданным начальным значением, вы можете сделать это:
AtomicInteger atomicInteger = new AtomicInteger(123); 1
В этом примере передается 123 в качестве параметра конструктору AtomicInteger, который устанавливает начальное значение экземпляра AtomicInteger равным 123.
24.2 Получение значения AtomicInteger
Вы можете использовать метод get() для получения значения экземпляра AtomicInteger. Пример выглядит следующим образом:
AtomicInteger atomicInteger = new AtomicInteger(123);
int theValue = atomicInteger.get(); 12
24.3 Установка значения AtomicInteger
Вы можете сбросить значение AtomicInteger с помощью метода set(). Вот пример AtomicInteger.set():
AtomicInteger atomicInteger = new AtomicInteger(123);
atomicInteger.set(234); 12
В приведенном выше примере создается AtomicInteger с начальным значением 123 и обновляется его значение до 234 во второй строке.
24.4 Сравнение и установка значения AtomicInteger
Класс AtomicInteger также передает атомарный метод compareAndSet(). Этот метод сравнивает текущее значение экземпляра AtomicInteger с ожидаемым значением и, если они равны, устанавливает новое значение для экземпляра AtomicInteger. Пример кода AtomicInteger.compareAndSet():
AtomicInteger atomicInteger = new AtomicInteger(123);
int expectedValue = 123;
int newValue = 234;
atomicInteger.compareAndSet(expectedValue, newValue); 12345
Этот пример начинается с создания экземпляра AtomicInteger с начальным значением 123. Затем AtomicInteger сравнивается с ожидаемым значением 123, и, если оно равно, значение AtomicInteger обновляется до 234.
24.5 Увеличение значения AtomicInteger
Класс AtomicInteger содержит методы, с помощью которых можно увеличить значение AtomicInteger и получить его значение. Эти методы следующие:
- addAndGet()
- getAndAdd()
- getAndIncrement()
- incrementAndGet()
Первый метод addAndGet() добавляет значение к AtomicInteger и возвращает увеличенное значение. Метод getAndAdd() добавляет значение к AtomicInteger, но возвращает значение предыдущего AtomicInteger, которое было увеличено. Какой из них использовать, зависит от сценария вашего приложения. Ниже приведены примеры обоих методов:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndAdd(10));
System.out.println(atomicInteger.addAndGet(10)); 123
В этом примере будут напечатаны 0 и 20. В примере вторая строка получает значение AtomicInteger перед добавлением 10. Значение перед добавлением 10 равно 0. Третья строка добавляет 10 к значению AtomicInteger и возвращает значение после добавления. Теперь значение равно 20.
Конечно, вы также можете использовать эти два метода для добавления отрицательных значений в AtomicInteger. Результат на самом деле является операцией вычитания.
Методы getAndIncrement() и incrementAndGet() аналогичны getAndAdd() и addAndGet(), но только каждый раз увеличивают значение AtomicInteger на 1.
24.6 Уменьшение значения AtomicInteger
Класс AtomicInteger также предоставляет некоторые атомарные методы для уменьшения значения AtomicInteger. Эти методы:
- decrementAndGet()
- getAndDecrement()
decrementAndGet() уменьшает значение AtomicInteger на единицу и возвращает уменьшенное значение. getAndDecrement() также уменьшает значение AtomicInteger на единицу, но возвращает значение до уменьшения.
25. Атомная длинная
Класс AtomicLong предоставляет нам переменную long, которая может выполнять атомарные операции чтения и записи. Он также содержит ряд расширенных атомарных операций, таких как compareAndSet(). Класс AtomicLong находится в пакете java.util.concurrent.atomic, поэтому его полный класс называется java.util.concurrent.atomic.AtomicLong. AtomicLong, описанный в этом разделе, относится к версии Java 8, а не к версии Java 5, где он был впервые представлен.
Философия дизайна, лежащая в основе AtomicLong, находится в моем понимании.Руководство по параллелизму JavaТема«Сравни и обменяй»Разделы имеют пояснения.
Создайте AtomicLong Создайте AtomicLong следующим образом:
AtomicLong atomicLong = new AtomicLong(); 1
создаст AtomicLong с начальным значением 0. Если вы хотите создать AtomicLong с указанным начальным значением, вы можете:
AtomicLong atomicLong = new AtomicLong(123); 1
В этом примере передается 123 в качестве параметра конструктору AtomicLong, который устанавливает начальное значение экземпляра AtomicLong равным 123. Получить значение AtomicLong Вы можете получить значение AtomicLong с помощью метода get(). Пример AtomicLong.get():
AtomicLong atomicLong = new AtomicLong(123);
long theValue = atomicLong.get(); 123
Установите значение AtomicLong Вы можете установить значение экземпляра AtomicLong с помощью метода set(). Пример AtomicLong.set() :
AtomicLong atomicLong = new AtomicLong(123);
atomicLong.set(234); 123
В этом примере создается новый AtomicLong с начальным значением 123, а во второй строке для него задается значение 234.
25.1 Сравнение и установка значения AtomicLong
Класс AtomicLong также имеет атомарный метод compareAndSet(). Этот метод сравнивает текущее значение экземпляра AtomicLong с ожидаемым значением и, если они равны, устанавливает новое значение для экземпляра AtomicLong. Пример использования AtomicLong.compareAndSet():
AtomicLong atomicLong = new AtomicLong(123);
long expectedValue = 123;
long newValue = 234;
atomicLong.compareAndSet(expectedValue, newValue); 12345
В этом примере создается новый AtomicLong с начальным значением 123. Затем текущее значение AtomicLong сравнивается с ожидаемым значением 123, и, если оно равно, новое значение AtomicLong становится равным 234.
25.2 Увеличьте значение AtomicLong
AtomicLong имеет методы, которые увеличивают значение AtomicLong и возвращают собственное значение. Эти методы следующие:
- addAndGet()
- getAndAdd()
- getAndIncrement()
- incrementAndGet()
Первый метод, addAndGet(), добавляет число к значению AtomicLong и возвращает увеличенное значение. Второй метод, getAndAdd(), также добавляет число к значению AtomicLong, но возвращает значение AtomicLong перед приращением. Какой из них использовать, зависит от вашего собственного сценария. Пример выглядит следующим образом:
AtomicLong atomicLong = new AtomicLong();
System.out.println(atomicLong.getAndAdd(10));
System.out.println(atomicLong.addAndGet(10)); 123
В этом примере будут напечатаны 0 и 20. В примере вторая строка получает значение AtomicLong перед добавлением 10. Значение перед добавлением 10 равно 0. Третья строка добавляет 10 к значению AtomicLong и возвращает значение после добавления. Теперь значение равно 20.
Конечно, вы также можете добавить отрицательные значения в AtomicLong, используя эти два метода. Результат на самом деле является операцией вычитания.
Методы getAndIncrement() и incrementAndGet() аналогичны getAndAdd() и addAndGet(), но только каждый раз увеличивают значение AtomicLong на 1.
25.3 Уменьшение значения AtomicLong
Класс AtomicLong также предоставляет некоторые атомарные методы для уменьшения значения AtomicLong. Эти методы:
- decrementAndGet()
- getAndDecrement()
decrementAndGet() уменьшает значение AtomicLong на единицу и возвращает уменьшенное значение. getAndDecrement() также уменьшает значение AtomicLong на единицу, но возвращает значение до уменьшения.
26. Атомная ссылка
AtomicReference предоставляет переменную ссылки на объект, которую можно читать и записывать атомарно. Атомарность означает, что несколько потоков, пытающихся изменить одну и ту же AtomicReference, не оставят AtomicReference в несогласованном состоянии. AtomicReference также имеет метод compareAndSet(), с помощью которого вы можете сравнить текущую ссылку с ожидаемым значением (ссылкой) и, если они равны, установить новую ссылку внутри объекта AtomicReference.
26.1 Создание атомной ссылки
Создайте AtomicReference следующим образом:
AtomicReference atomicReference = new AtomicReference(); 1
Если вам нужно создать AtomicReference с определенной ссылкой, вы можете:
String initialReference = "the initially referenced string";
AtomicReference atomicReference = new AtomicReference(initialReference); 12
26.2 Создание общего AtomicReference
Вы можете использовать дженерики Java для создания универсального AtomicReference. Пример:
AtomicReference<String> atomicStringReference =
new AtomicReference<String>(); 12
Вы также можете установить начальное значение для общего AtomicReference. Пример:
String initialReference = "the initially referenced string";
AtomicReference<String> atomicStringReference = new AtomicReference<String>(initialReference); 12
26.3 Получить ссылку на AtomicReference
Вы можете получить ссылку, хранящуюся в AtomicReference, с помощью метода get() AtomicReference. Если ваш AtomicReference не является общим, метод get() вернет ссылку типа Object. Если общий, get() вернет тип, который вы объявили при создании AtomicReference.
Давайте сначала посмотрим на неуниверсальный пример AtomicReference get():
AtomicReference atomicReference = new AtomicReference("first value referenced");
String reference = (String) atomicReference.get(); 12
Обратите внимание, как ссылка, возвращаемая методом get(), преобразуется в String. Обобщенный пример AtomicReference:
AtomicReference<String> atomicReference = new AtomicReference<String>("first value referenced");
String reference = atomicReference.get(); 12
Компилятор знает тип ссылки, поэтому нам больше не нужно приводить ссылку, возвращаемую get().
26.4 Установка AtomicReference
Вы можете использовать метод get() для установки ссылки, сохраненной в AtomicReference. Если вы определяете неуниверсальную AtomicReference, set() будет принимать ссылку на объект в качестве аргумента. В случае универсального AtomicReference метод set() будет принимать только тот тип, для которого вы его определили.
Пример AtomicReference set():
AtomicReference atomicReference = new AtomicReference();
atomicReference.set("New object referenced"); 12
- Кажется, что нет никакой разницы между неуниверсальным и универсальным. Настоящая разница в том, что компилятор накладывает ограничения на типы параметров, которые вы можете установить для общего AtomicReference.
26.5 Сравнение и настройка ссылок AtomicReference
В классе AtomicReference есть очень полезный метод: compareAndSet(). compareAndSet() может сравнить ссылку, хранящуюся в AtomicReference, с ожидаемой ссылкой, если две ссылки совпадают (не equals(), а ==), он установит новый экземпляр AtomicReference Quote.
compareAndSet() вернет true, если compareAndSet() установит новую ссылку на AtomicReference. В противном случае compareAndSet() возвращает false.
Пример AtomicReference compareAndSet():
String initialReference = "initial value referenced";
AtomicReference<String> atomicStringReference =
new AtomicReference<String>(initialReference);
String newReference = "new value referenced";
boolean exchanged = atomicStringReference.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);
exchanged = atomicStringReference.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged); 1234567891011
В этом примере создается универсальный AtomicReference с начальной ссылкой. Затем два раза вызывается comparesAndSet(), чтобы сравнить сохраненное значение с ожидаемым и, если они совпадают, установить новую ссылку на AtomicReference. Для первого сравнения сохраненная ссылка (initialReference) соответствует ожидаемой ссылке (initialReference), поэтому новая ссылка (newReference) устанавливается на AtomicReference, а метод compareAndSet() возвращает значение true. Во втором сравнении сохраненная ссылка (newReference) не соответствует ожидаемой ссылке (initialReference), поэтому новой ссылке не присваивается значение AtomicReference, а метод compareAndSet() возвращает false.