В Java наиболее используемой структурой данных, вероятно, является список. Список имеет неопределенное количество элементов, и вы можете добавлять, читать или удалять элементы любой позиции. Кроме того, параллельные списки позволяют различным потокам добавлять или удалять элементы в списке одновременно, не создавая никакой несогласованности данных. А неблокирующие списки предоставляют операции, которые, если операция не может быть выполнена немедленно, выдают исключение или возвращают значение null, в зависимости от операции. Java 7 представил класс ConcurrentLinkedDeque, который реализует неблокирующий параллельный список, и в этом руководстве мы научимся использовать этот класс.
Пример ConcurrentLinkedDeque
В этом примере мы реализуем пример со следующими двумя различными задачами:
- Тот, который добавляет данные в список в большом объеме.
- Тот, который удаляет данные из того же списка в большом объеме.
Давайте создадим темы для каждой задачи.
package com.howtodoinjava.demo.multithreading.concurrentLinkedDequeExample;import java.util.concurrent.ConcurrentLinkedDeque;public class AddTask implements Runnable {private ConcurrentLinkedDeque<String> list;public AddTask(ConcurrentLinkedDeque<String> list) {this.list = list;}@Overridepublic void run() {String name = Thread.currentThread().getName();for(int i = 0; i < 10000; i++) {list.add(name + ": Element " + i);}}}
и
package com.howtodoinjava.demo.multithreading.concurrentLinkedDequeExample;import java.util.concurrent.ConcurrentLinkedDeque;public class RemoveTask implements Runnable {private ConcurrentLinkedDeque<String> list;public RemoveTask(ConcurrentLinkedDeque<String> list) {this.list = list;}@Overridepublic void run() {for(int i = 0; i < 5000; i++) {list.pollFirst();list.pollLast();}}}
Теперь давайте создадим 100 потоков, добавляющих данные в список, и 100 потоков для удаления данных из списка. Если список действительно потокобезопасен и неблокируемый, он даст вам конечный результат практически мгновенно. Более того, размер списка в конечном итоге будет равен нулю.
package com.howtodoinjava.demo.multithreading.concurrentLinkedDequeExample;import java.util.concurrent.ConcurrentLinkedDeque;public class Main {public static void main(String[] args){ConcurrentLinkedDeque<String> list = new ConcurrentLinkedDeque<>();Thread threads[] = new Thread[100];for(int i = 0; i < threads.length; i++) {AddTask task = new AddTask(list);threads[i] = new Thread(task);threads[i].start();}System.out.printf("Main: %d AddTask threads have been launched\n", threads.length);for(int i = 0; i < threads.length; i++) {try {threads[i].join();} catch(InterruptedException e) {e.printStackTrace();}}System.out.printf("Main: Size of the List: %d\n", list.size());for(int i = 0; i < threads.length; i++) {RemoveTask task = new RemoveTask(list);threads[i] = new Thread(task);threads[i].start();}System.out.printf("Main: %d RemoveTask threads have been launched\n", threads.length);for(int i = 0; i < threads.length; i++) {try {threads[i].join();} catch(InterruptedException e) {e.printStackTrace();}}System.out.printf("Main: Size of the List: %d\n", list.size());}}Output:Main: 100 AddTask threads have been launchedMain: Size of the List: 1000000Main: 100 RemoveTask threads have been launchedMain: Size of the List: 0
Давайте посмотрим, как все это работало:
- Сначала вы выполнили 100 задач AddTask для добавления элементов в список. Каждая из этих задач вставляет 10 000 элементов в список с помощью метода add(). Этот метод добавляет новые элементы в конец списка. Когда все эти задачи будут завершены, вы запишете в консоль количество элементов списка. На данный момент список содержит 1 000 000 элементов.
- Затем вы выполнили 100 задач RemoveTask для удаления элементов из списка. Каждая из этих задач удаляет 10 000 элементов списка с помощью методов pollFirst() и pollLast(). Метод pollFirst() возвращает и удаляет первый элемент списка, а метод pollLast() возвращает и удаляет последний элемент списка. Если список пуст, эти методы возвращают нулевое значение. Когда все эти задачи будут завершены, вы запишете в консоль количество элементов списка. На данный момент в списке ноль элементов.
- Чтобы записать количество элементов списка, вы использовали метод size(). Вы должны принять во внимание, что этот метод может возвращать нереальное значение, особенно если вы используете его, когда есть потоки, добавляющие или удаляющие данные в списке. Метод должен обойти весь список, чтобы подсчитать элементы, и содержимое списка может измениться для этой операции. Только если вы используете их, когда нет потоков, изменяющих список, у вас будет гарантия, что возвращаемый результат будет правильным.
Обратите внимание, что класс ConcurrentLinkedDeque предоставляет больше методов для получения элементов из списка:
- getFirst() и getLast(): Эти методы возвращают первый и последний элемент из списка соответственно. Они не удаляют возвращенный элемент из списка. Если список пуст, эти методы выдают исключение NoSuchElementExcpetion.
- peek(), peekFirst() и peekLast(): Эти методы возвращают первый и последний элемент списка соответственно. Они не удаляют возвращаемый элемент из списка. Если список пуст, эти методы возвращают значение null.
- remove(), removeFirst(), removeLast(): Эти методы возвращают первый и последний элемент списка соответственно. Они удаляют возвращенный элемент из списка. Если список пуст, эти методы выдают исключение NoSuchElementException.
- ConcurrentLinkedDeque является подходящим выбором, когда множество потоков будут совместно использовать доступ к общей коллекции.
- Как и большинство других реализаций параллельных коллекций, этот класс не допускает использования нулевых элементов.
- Итераторы слабо согласованы, возвращая элементы, отражающие состояние дека в какой-то момент во время или после создания итератора. Они не выдают ConcurrentModificationException и могут продолжаться одновременно с другими операциями.