Перейти к содержимому

Atomic java как работает

  • автор:

Как устроены атомики?

Начнем с того, что такое атомики и зачем нужны. Atomic* – семейство классов из java.util.concurrent . Они предоставляют набор атомарных операций для соответствующих типов. Например с помощью методов getAndIncrement / incrementAndGet класса AtomicInteger можно делать неатомарный в обычных условиях инкремент ( i++ ).

Условно можно разделить подходы реализации большинства atomic-методов на две группы: compare-and-set и set-and-get.

Методы категории compare-and-set принимают старое значение и новое. Если переданное старое значение совпало с текущим, устанавливается новое. Обычно делегируют вызов в методы класса Unsafe , которые заменяются нативными реализациями виртуальной машины. Виртуальная машина в большинстве случаев использует атомарную операцию процессора compare-and-swap (CAS). Поэтому атомики обычно более эффективны чем стандартная дорогостоящая блокировка.

В случае set-and-get старое значение неизвестно. Поэтому нужен небольшой трюк: программа сначала считывает текущее значение, а затем записывает новое, тоже с помощью CAS, потому что запись могла успеть поменяться даже за этот шаг. Эта попытка чтения+записи повторяется в цикле, пока старое значение не совпадет и переменная не будет успешно записана.

Этот трюк называется double-checked или optimistic locking, и может быть использован и в пользовательском коде с любым способом синхронизации. Оптимистичность заключается в том, что мы надеемся что состояния гонки нет, прибегая к синхронизации только если гонка всё же случилась. Реализация оптимистичной блокировки может быть дана как отдельная задача.

Многопоточное программирование в Java 8. Часть третья. Атомарные переменные и конкурентные таблицы

На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

Добро пожаловать в третью часть руководства по параллельному программированию в Java 8. В первой части мы рассматривали, как выполнять код параллельно с помощью потоков, задач и сервисов исполнителей. Во второй разбирались с тем, как синхронизировать доступ к изменяемым объектам с помощью ключевого слова synchronized , блокировок и семафоров. Сегодня, в заключительной части, я расскажу о двух очень важных частях Concurrency API: об атомарных переменных и о конкурентных таблицах (Concurrent Maps).

AtomicInteger

Пакет java.concurrent.atomic содержит много полезных классов для выполнения атомарных операций. Операция называется атомарной тогда, когда её можно безопасно выполнять при параллельных вычислениях в нескольких потоках, не используя при этом ни блокировок, ни synchronized , как мы это делали в предыдущем уроке.

Внутри атомарные классы очень активно используют сравнение с обменом (compare-and-swap, CAS), атомарную инструкцию, которую поддерживает большинство современных процессоров. Эти инструкции работают гораздо быстрее, чем синхронизация с помощью блокировок. Поэтому, если вам просто нужно изменять одну переменную с помощью нескольких потоков, лучше выбирать атомарные классы.

Приведу несколько примеров с использованием AtomicInteger , одного из атомарных классов:

AtomicInteger atomicInt = new AtomicInteger(0); ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 1000) .forEach(i -> executor.submit(atomicInt::incrementAndGet)); stop(executor); System.out.println(atomicInt.get()); // => 1000 

Как видите, использование AtomicInteger вместо обычного Integer позволило нам корректно увеличить число, распределив работу сразу по двум потокам. Мы можем не беспокоиться о безопасности, потому что incrementAndGet() является атомарной операцией.

Класс AtomicInteger поддерживает много разных атомарных операций. Метод updateAndGet() принимает в качестве аргумента лямбда-выражение и выполняет над числом заданные арифметические операции:

AtomicInteger atomicInt = new AtomicInteger(0); ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 1000) .forEach(i -> < Runnable task = () ->atomicInt.updateAndGet(n -> n + 2); executor.submit(task); >); stop(executor); System.out.println(atomicInt.get()); // => 2000 

Метод accumulateAndGet() принимает лямбда-выражения типа IntBinaryOperator . Вот как мы можем использовать его, чтобы просуммировать все числа от нуля до тысячи:

AtomicInteger atomicInt = new AtomicInteger(0); ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 1000) .forEach(i -> < Runnable task = () ->atomicInt.accumulateAndGet(i, (n, m) -> n + m); executor.submit(task); >); stop(executor); System.out.println(atomicInt.get()); // => 499500 

Среди других атомарных классов хочется упомянуть такие как AtomicBoolean, AtomicLong и AtomicReference.

LongAdder

Класс LongAdder может выступать в качестве альтернативы AtomicLong для последовательного сложения чисел.

ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 1000) .forEach(i -> executor.submit(adder::increment)); stop(executor); System.out.println(adder.sumThenReset()); // => 1000 

Так же, как и у других атомарных чисел, у LongAdder есть методы increment() и add() . Но вместо того, чтобы складывать числа сразу, он просто хранит у себя набор слагаемых, чтобы уменьшить взаимодействие между потоками. Узнать результат можно с помощью вызова sum() или sumThenReset() . Этот класс используется в ситуациях, когда добавлять числа приходится гораздо чаще, чем запрашивать результат (часто это какие-то статистические исследование, например подсчёт количества запросов). Несложно догадаться, что, давая прирост в производительности, LongAdder требует гораздо большего количества памяти из-за того, что он хранит все слагаемые.

LongAccumulator

Класс LongAccumulator несколько расширяет возможности LongAdder . Вместо простого сложения он обрабатывает входящие значения с помощью лямбды типа LongBinaryOperator , которая передаётся при инициализации. Выглядит это так:

LongBinaryOperator op = (x, y) -> 2 * x + y; LongAccumulator accumulator = new LongAccumulator(op, 1L); ExecutorService executor = Executors.newFixedThreadPool(2); IntStream.range(0, 10) .forEach(i -> executor.submit(() -> accumulator.accumulate(i))); stop(executor); System.out.println(accumulator.getThenReset()); // => 2539 

В этом примере при каждом вызове accumulate() значение аккумулятора увеличивается в два раза, и лишь затем суммируется с i . Так же, как и LongAdder , LongAccumulator хранит весь набор переданных значений в памяти.

На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

На самом деле, пример не совсем корректный; согласно документации, LongAccumulator не гарантирует порядка выполнения операций. Корректной формулой была бы, например x+2*y, т.к. при любом порядке выполнения в конце будет получаться одно и то же значение.

ConcurrentMap

Интерфейс ConcurrentMap наследуется от обычного Map и предоставляет описание одной из самой полезной коллекции для конкурентного использования. Чтобы продемонстрировать новые методы интерфейса, мы будем использовать вот эту заготовку:

ConcurrentMap map = new ConcurrentHashMap<>(); map.put("foo", "bar"); map.put("han", "solo"); map.put("r2", "d2"); map.put("c3", "p0"); 

Метод forEach() принимает лямбду типа BiConsumer . Этой лямбде будут передаваться в качестве аргументов все ключи и значения таблицы по очереди. Этот метод может использоваться как замена for-each циклам с итерацией по всем Entry . Итерация выполняется последовательно, в текущем потоке. эту запятую не надо убирать. если вы считаете, что надо — пожалуйста, сначала скажите мне

map.forEach((key, value) -> System.out.printf("%s = %s\n", key, value)); 

Метод putIfAbsent() помещает в таблицу значение, только если по данному ключу ещё нет другого значения. Этот метод является потокобезопасным (о крайней мере, в реализации ConcurrentHashMap ), поэтому вам не нужно использовать synchronized , когда вы хотите использовать его в нескольких потоках (то же самое справедливо и для обычного put() ):

String value = map.putIfAbsent("c3", "p1"); System.out.println(value); // p0 

Метод getOrDefault() работает так же, как и обычный get() , с той лишь разницей, что при отсутствии значения по данному ключу он вернёт значение по-умолчанию, передаваемое вторым аргументом:

String value = map.getOrDefault("hi", "there"); System.out.println(value); // there 

Метод replaceAll() принимает в качестве аргумента лямбда-выражение типа BiFunction . Этой лямбде по очереди передаются все комбинации ключ-значения из карты, а результат, который она возвращает, записывается соответствующему ключу в качестве значения:

map.replaceAll((key, value) -> "r2".equals(key) ? "d3" : value); System.out.println(map.get("r2")); // d3 

Если же вам нужно изменить таким же образом только один ключ, это позволяет сделать метод compute() :

map.compute("foo", (key, value) -> value + value); System.out.println(map.get("foo")); // barbar 

Кроме обычного compute() , существуют так же методы computeIfAbsent() и computeIfPresent() . Они изменяют значение только если значение по данному ключу отсутствует (или присутствует, соответственно).

И, наконец, метод merge() , который может быть использован для объединения существующего ключа с новым значением. В качестве аргумента он принимает ключ, новое значение и лямбду, которая определяет, как новое значение должно быть объединено со старым:

map.merge("foo", "boo", (oldVal, newVal) -> newVal + " was " + oldVal); System.out.println(map.get("foo")); // boo was bar 

ConcurrentHashMap

Кроме методов, которые описаны в ConcurrencyMap , в ConcurrentHashMap было добавлено и ещё несколько своих. Так же, как и параллельные stream’ы, эти методы используют специальный ForkJoinPool , доступный через ForkJoinPool.commonPool() в Java 8. Этот пул использует свои настройки для количества потоков, основанные на количестве ядер. У меня их 4, а значит использоваться будет три потока:

System.out.println(ForkJoinPool.getCommonPoolParallelism()); // 3 

Это значение может быть специально изменено с помощью параметра JVM:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=5 

Мы рассмотрим три новых метода: forEach , search and reduce . У каждого из них есть первый аргумент, который называется parallelismThreshold , который определяет минимальное количество элементов в коллекции, при котором операция будет выполняться в нескольких потоках. Т.е. если в коллекции 499 элементов, а первый параметр выставлен равным пятистам, то операция будет выполняться в одном потоке последовательно. В наших примерах мы будем использовать первый параметр равным в единице, чтобы операции всегда выполнялись параллельно.

Для примеров ниже мы будем использовать всё ту же таблицу, что и выше (однако объявим её именем класса, а не интерфейса. чтобы нам были доступны все методы):

ConcurrentHashMap map = new ConcurrentHashMap<>(); map.put("foo", "bar"); map.put("han", "solo"); map.put("r2", "d2"); map.put("c3", "p0"); 
ForEach

Работает метод так же, как и в ConcurrentMap . Для иллюстрации многопоточности мы будем выводить названия потоков (не забывайте, что их количество для меня ограничено тремя):

map.forEach(1, (key, value) -> System.out.printf("key: %s; value: %s; thread: %s\n", key, value, Thread.currentThread().getName())); // key: r2; value: d2; thread: main // key: foo; value: bar; thread: ForkJoinPool.commonPool-worker-1 // key: han; value: solo; thread: ForkJoinPool.commonPool-worker-2 // key: c3; value: p0; thread: main 
Search

Метод search() принимает лямбда-выражение типа BiFunction , в которую передаются все пары ключ-значение по очереди. Функция должна возвращать null , если необходимое вхождение найдено. В случае, если функция вернёт не null , дальнейший поиск будет остановлен. Не забывайте, что данные в хэш-таблице хранятся неупорядоченно. Если вы будете полагаться на порядок, в котором вы добавляли данные в неё, вы можете не получить ожидаемого результата. Если условиям поиска удовлетворяют несколько вхождений, результат точно предсказать нельзя.

String result = map.search(1, (key, value) -> < System.out.println(Thread.currentThread().getName()); if ("foo".equals(key)) < return value; >return null; >); System.out.println("Result: " + result); // ForkJoinPool.commonPool-worker-2 // main // ForkJoinPool.commonPool-worker-3 // Result: bar 

Или вот другой пример, который полагается только на значения:

String result = map.searchValues(1, value -> < System.out.println(Thread.currentThread().getName()); if (value.length() >3) < return value; >return null; >); System.out.println("Result: " + result); // ForkJoinPool.commonPool-worker-2 // main // main // ForkJoinPool.commonPool-worker-1 // Result: solo 
Reduce

Метод reduce() вы могли уже встречать в Java 8 Streams. Он принимает две лямбды типа BiFunction . Первая функция преобразовывает пару ключ/значение в один объект (любого типа). Вторая функция совмещает все полученные значения в единый результат, игнорируя любые возможные null -значения.

tring result = map.reduce(1, (key, value) -> < System.out.println("Transform: " + Thread.currentThread().getName()); return key + "=" + value; >, (s1, s2) -> < System.out.println("Reduce: " + Thread.currentThread().getName()); return s1 + ", " + s2; >); System.out.println("Result: " + result); // Transform: ForkJoinPool.commonPool-worker-2 // Transform: main // Transform: ForkJoinPool.commonPool-worker-3 // Reduce: ForkJoinPool.commonPool-worker-3 // Transform: main // Reduce: main // Reduce: main // Result: r2=d2, c3=p0, han=solo, foo=bar 

На этом всё. Надеюсь, мои статьи были вам полезны ?

Java Blog

Чтобы разобрать использование атомарных операций в Java рассмотрим следующий пример:

public class Counter < int counter; public void increment() < counter++; >>

В случае однопоточной среды это работает отлично; однако, как только мы разрешаем запись более чем одному потоку, мы начинаем получать противоречивые результаты.

Причина этому в операции приращения (counter++), которая может выглядеть как атомарная операция, но на самом деле представляет собой комбинацию трех операций: получение значения, увеличение и обратная запись обновленного значения.

Если два потока попытаются получить и обновить значение одновременно, это может привести к потере обновлений.

Один из способов управления доступом к объекту — использование блокировок. Этого можно достичь, используя ключевое слово synchronized в сигнатуре метода приращения. Ключевое слово synchronized гарантирует, что только один поток может входить в метод одновременно:

public class SynchronizedCounterWithLock < private volatile int counter; public synchronized void increment() < counter++; >>

Кроме того, нам нужно добавить ключевое слово volatile, чтобы обеспечить надлежащую видимость ссылок среди потоков.

Использование блокировок решает проблему. Однако производительность страдает в этом случае.

Когда несколько потоков пытаются получить блокировку, один из них выигрывает, а остальные потоки либо блокируются, либо приостанавливаются.

Процесс приостановки и последующего возобновления потока очень дорог и влияет на общую эффективность системы.

В небольшой программе, такой как счетчик, время, затрачиваемое на переключение контекста, может быть намного больше, чем фактическое выполнение кода, что значительно снижает общую эффективность.

Атомарные операции

Существует направление исследований, посвященное созданию неблокирующих алгоритмов для конкурентных сред. Эти алгоритмы используют низкоуровневые атомарные машинные инструкции, такие как сравнение и замена (CAS, compare-and-swap), для обеспечения целостности данных.

Типичная операция CAS работает с тремя операндами:

  • Место в памяти для работы (M)
  • Существующее ожидаемое значение (A) переменной
  • Новое значение (B), которое необходимо установить

Операция CAS атомарно обновляет значение в M до B, но только если существующее значение в M совпадает с A, в противном случае никаких действий не предпринимается.

В обоих случаях возвращается существующее значение в M. Это объединяет три шага — получение значения, сравнение значения и обновление значения — в одну операцию на машинном уровне.

Когда несколько потоков пытаются обновить одно и то же значение через CAS, один из них выигрывает и обновляет значение. Однако, в отличие от блокировок, никакой другой поток не приостанавливается; вместо этого им просто сообщают, что им не удалось обновить значение. Затем потоки могут перейти к дальнейшей работе, и переключение контекста полностью исключено.

Еще одно последствие состоит в том, что основная логика программы становится более сложной. Это потому, что мы должны обработать сценарий, когда операция CAS не удалась. Мы можем повторять его снова и снова, пока он не увенчается успехом, или мы можем ничего не делать и двигаться дальше в зависимости от варианта использования.

AtomicInteger

Рассмотрим методы приращения и уменьшения в классе AtomicInteger.

Первая группа методов обновляет значение и возвращает то, которое было до обновления:

public final int getAndIncrement() < for (;;) < int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; >> public final int getAndDecrement() < for (;;) < int current = get(); int next = current - 1; if (compareAndSet(current, next)) return current; >> public final int getAndAdd(int delta) < for (;;) < int current = get(); int next = current + delta; if (compareAndSet(current, next)) return current; >>

Вторая группа методов обновляет значение и возвращает то, которое стало после обновления:

public final int incrementAndGet() < for (;;) < int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; >> public final int decrementAndGet() < for (;;) < int current = get(); int next = current - 1; if (compareAndSet(current, next)) return next; >> public final int addAndGet(int delta) < for (;;) < int current = get(); int next = current + delta; if (compareAndSet(current, next)) return next; >>

Как видно в обоих случаях используется функция compareAndSet, представляющая операцию CAS (compare-and-swap), которая использует unsafe пакет:

public final boolean compareAndSet(int expect, int update)

  • Пакет java.util.concurrent.atomic
  • Spring Resource: предостережения относительно FileSystemResource
  • Модульное (юнит) тестирование в Spring

Атомарные классы пакета util.concurrent

Пакет java.util.concurrent.atomic содержит девять классов для выполнения атомарных операций. Операция называется атомарной, если её можно безопасно выполнять при параллельных вычислениях в нескольких потоках, не используя при этом ни блокировок, ни синхронизацию synchronized. Прежде, чем перейти к рассмотрению атомарных классов, рассмотрим выполнение наипростейших операций инкремента и декремента целочисленных значений.

С точки зрения программиста операции инкремента (i++, ++i) и декремента (i—, —i) выглядят наглядно и компактно. Но, с точки зрения JVM (виртуальной машины Java) данные операции не являются атомарными, поскольку требуют выполнения нескольких действительно атомарных операции: чтение текущего значения, выполнение инкремента/декремента и запись полученного результата. При работе в многопоточной среде операции инкремента и декремента могут стать источником ошибок. Т.е. в многопоточной среде простые с виду операции инкремента и декремента требуют использование синхронизации и блокировки. Но блокировки содержат массу недостатков, и для простейших операций инкремента/декремента являются тяжеловесными. Выполнение блокировки связано со средствами операционной системы и несёт в себе опасность приостановки с невозможностью дальнейшего возобновления потока, а также опасность взаимоблокировки или инверсии приоритетов (priority inversion). Кроме этого, появляются дополнительные расходы на переключение потоков. Но можно ли обойтись без блокировок? В ряде случаев можно!

Блокировка подразумевает пессимистический подход, разрешая только одному потоку выполнять определенный код, связанный с изменением значения некоторой «общей» переменной. Таким образом, никакой другой поток не имеет доступа к определенным переменным. Но можно использовать и оптимистический подход. В этом случае блокировки не происходит, и если поток обнаруживает, что значение переменной изменилось другим потоком, то он повторяет операцию снова, но уже с новым значением переменной. Так работают атомарные классы.

Описание атомарного класса AtomicLong

Рассмотрим принцип действия механизма оптимистической блокировки на примере атомарного класса AtomicLong, исходный код которого представлен ниже. В этом классе переменная value объявлена с модификатором volatile, т.е. её значение могут поменять разные потоки одновременно. Модификатор volatile гарантирует выполнение отношения happens-before, что ведет к тому, что измененное значение этой переменной увидят все потоки.

Каждый атомарный класс включает метод compareAndSet, представляющий механизм оптимистичной блокировки и позволяющий изменить значение value только в том случае, если оно равно ожидаемому значению (т.е. current). Если значение value было изменено в другом потоке, то оно не будет равно ожидаемому значению. Следовательно, метод compareAndSet вернет значение false, что приведет к новой итерации цикла while в методе getAndAdd. Таким образом, в очередном цикле в переменную current будет считано обновленное значение value, после чего будет выполнено сложение и новая попытка записи получившегося значения (т.е. next). Переменные current и next — локальные, и, следовательно, у каждого потока свои экземпляры этих переменных.

private volatile long value; public final long get() < return value; >public final long getAndAdd(long delta) < while (true) < long current = get(); long next = current + delta; if (compareAndSet(current, next)) return current; >>

Метод compareAndSet реализует механизм оптимистической блокировки. Знакомые с набором команд процессоров специалисты знают, что ряд архитектур имеют инструкцию Compare-And-Swap (CAS), которая является реализацией этой самой операции. Таким образом, на уровне инструкций процессора имеется поддержка необходимой атомарной операции. На архитектурах, где инструкция не поддерживается, операции реализованы иными низкоуровневыми средствами.

Основная выгода от атомарных (CAS) операций появляется только при условии, когда переключать контекст процессора с потока на поток становится менее выгодно, чем немного покрутиться в цикле while, выполняя метод boolean compareAndSwap(oldValue, newValue). Если время, потраченное в этом цикле, превышает 1 квант потока, то, с точки зрения производительности, может быть невыгодно использовать атомарные переменные.

Список атомарных классов

Атомарные классы пакета java.util.concurrent.atomic можно разделить на 4 группы :

• AtomicBoolean
• AtomicInteger
• AtomicLong
• AtomicReference
Atomic-классы для boolean, integer, long и ссылок на объекты.
Классы этой группы содержат метод compareAndSet, принимающий 2 аргумента : предполагаемое текущее и новое значения. Метод устанавливает объекту новое значение, если текущее равно предполагаемому, и возвращает true. Если текущее значение изменилось, то метод вернет false и новое значение не будет установлено.
Кроме этого, классы имеют метод getAndSet, который безусловно устанавливает новое значение и возвращает старое.
Классы AtomicInteger и AtomicLong имеют также методы инкремента/декремента/добавления нового значения.
• AtomicIntegerArray
• AtomicLongArray
• AtomicReferenceArray
Atomic-классы для массивов integer, long и ссылок на объекты.
Элементы массивов могут быть изменены атомарно.
• AtomicIntegerFieldUpdater
• AtomicLongFieldUpdater
• AtomicReferenceFieldUpdater
Atomic-классы для обновления полей по их именам с использованием reflection.
Смещения полей для CAS операций определяется в конструкторе и кэшируются. Сильного падения производительности из-за reflection не наблюдается.
• AtomicStampedReference
• AtomicMarkableReference
Atomic-классы для реализации некоторых алгоритмов, (точнее сказать, уход от проблем при реализации алгоритмов).
Класс AtomicStampedReference получает в качестве параметров ссылку на объект и int значение.
Класс AtomicMarkableReference получает в качестве параметров ссылку на объект и битовый флаг (true/false).

Полная документация по атомарным классам на английском языке представлена на оффициальном сайте Oracle. Наиболее часто используемые классы (не трудно догадаться) сосредоточены в первой группе.

Производительность атомарных классов

Согласно множеству источников неблокирующие алгоритмы в большинстве случаев более масштабируемы и намного производительнее, чем блокировки. Это связано с тем, что операции CAS реализованы на уровне машинных инструкций, а блокировки тяжеловесны и используют приостановку и возобновление потоков, переключение контекста и т.д. Тем не менее, блокировки демонстрируют лучший результат только при очень «высокой конкуренции», что в реальной жизни встречается не так часто.

Основной недостаток неблокирующих алгоритмов связан со сложностью их реализации по сравнению с блокировками. Особенно это касается ситуаций, когда необходимо контролировать состояние не одного поля, а нескольких.

Пример неблокирующего генератора последовательности

Рассмотрим генерирующий последовательность [1, 2, 4, 8, 16, . ] класс SequenceGenerator, функционирующий в многопоточной среде.

Листинг класса SequenceGenerator для генерирования последовательности

Для работы в многопоточной среде без блокировок используем атомарную ссылку AtomicReference, которая обеспечит хранение целочисленного значения типа java.math.BigInteger. Метод next возвращает текущее значение; переменная next вычисляет следующее значение. Метод compareAndSet атомарного класса element обеспечивает сохранение нового значения, если текущее не изменилось. Таким образом, метод next возвращает текущее значение и увеличивает его в 2 раза.

import java.math.BigInteger; import java.util.concurrent.atomic.AtomicReference; public class SequenceGenerator < private static BigInteger MULTIPLIER; private AtomicReferenceelement; public SequenceGenerator() < if (MULTIPLIER == null) MULTIPLIER = BigInteger.valueOf(2); element = new AtomicReference( BigInteger.ONE); > public BigInteger next() < BigInteger value; BigInteger next; do < value = element.get(); next = value.multiply(MULTIPLIER); >while (!element.compareAndSet(value, next)); return value; > >
Листинг последовательности Sequence

Для тестирования генератора последовательности SequenceGenerator используем класс Sequence, реализующий интерфейс Runnable. В качестве параметра конструктор класса получает идентификатор потока id, размер последовательности count и генератор последовательности sg. В методе run в цикле с незначительными задержками формируется последовательность чисел sequence. После завершения цикла значения последовательности «выводятся» в консоль методом printSequence.

import java.math.BigInteger; import java.util.ArrayList; import java.util.List; class Sequence implements Runnable < Thread thread; int id; int count; SequenceGenerator sg; Listsequence; sequence = new ArrayList(); boolean printed = false; Sequence(final int id, final int count, SequenceGenerator sg) < this.count = count; this.id = id; this.sg = sg; thread = new Thread(this); System.out.println("Создан поток " + id); thread.start(); >@Override public void run() < try < for (int i = 0; i < count; i++) < sequence.add(sg.next()); Thread.sleep((long) ( (Math.random()*2 + 1)*30)); >> catch (InterruptedException e) < System.out.println("Поток " + id + " прерван"); >System.out.print("Поток " + id + " завершён"); printSequence(); > public void printSequence() < if (printed) return; String tmp = "["; for (int i = 0; i < sequence.size(); i++) < if (i >0) tmp += ", "; String nb = String.valueOf(sequence.get(i)); while (nb.length() < 9) nb = " " + nb; tmp += nb; >tmp += "]"; System.out.println("Последовательность потока " + id + " : " + tmp); printed = true; > >
Листинг примера SequenceGeneratorExample

В примере SequenceGeneratorExample сначала создается генератор последовательности SequenceGenerator. После этого в цикле формируется массив из десяти Sequence, которые в паралелльных потоках по три раза обращаются к генератору последовательсности.

public class SequenceGeneratorExample < public static void main(String[] args) < SequenceGenerator sg = new SequenceGenerator(); Listsequences = new ArrayList(); for (int i = 0; i < 10; i++) < Sequence seq = new Sequence(i + 1, 3, sg); sequences.add(seq); >System.out.println("\nРасчет последовательностей\n"); int summa; // Ожидания завершения потоков do < summa = 0; for (int i = 0; i < sequences.size(); i++) < if (!sequences.get(i).thread.isAlive()) < sequences.get(i).printSequence(); summa++; >> try < Thread.sleep(100); >catch (InterruptedException e) <> > while (summa < sequences.size()) ; System.out.println("\n\nРабота потоков завершена"); System.exit(0); >>
Результаты выполнения примера

При выполнении примера в консоль будет выведена следующая информация :

Создан поток 0 Создан поток 1 Создан поток 2 Создан поток 3 Создан поток 4 Создан поток 5 Создан поток 6 Создан поток 7 Создан поток 8 Создан поток 9 Расчет последовательностей Поток 7 завершён Последовательность потока 7 : [ 256, 4096, 524288] Поток 5 завершён Поток 4 завершён Поток 1 завершён Последовательность потока 1 : [ 2, 1024, 2097152] Последовательность потока 4 : [ 16, 8192, 8388608] Последовательность потока 5 : [ 64, 2048, 32768] Поток 9 завершён Поток 3 завершён Поток 6 завершён Последовательность потока 3 : [ 8, 131072, 134217728] Последовательность потока 6 : [ 32, 16384, 268435456] Последовательность потока 9 : [ 512, 262144, 16777216] Поток 0 завершён Поток 2 завершён Поток 8 завершён Последовательность потока 0 : [ 1, 65536, 67108864] Последовательность потока 2 : [ 4, 1048576, 33554432] Последовательность потока 8 : [ 128, 4194304, 536870912] Работа потоков завершена

Каждый поток в цикле сформировал целочисленный массив из 3-х значений при обращении к «атомарному» генератору последовательности. Как видно из результатов выполнения примера, значения не пересекаются.

Скачать примеры

Рассмотренный на странице пример использования атомарного класса в виде проекта Eclipse можно скачать здесь (7.41 Кб).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *