Руководство по Java SynchronousQueue

Руководство по Java SynchronousQueue

1. обзор

В этой статье мы рассмотримSynchronousQueue из пакетаjava.util.concurrent.

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

2. Обзор API

SynchronousQueue имеет толькоtwo supported operations: take() and put(), and both of them are blocking.

Например, когда мы хотим добавить элемент в очередь, нам нужно вызвать методput(). Этот метод будет блокироваться до тех пор, пока какой-либо другой поток не вызовет методtake(), сигнализируя, что он готов принять элемент.

ХотяSynchronousQueue имеет интерфейс очереди, мы должны рассматривать его как точку обмена для одного элемента между двумя потоками, в которой один поток передает элемент, а другой поток принимает этот элемент.

3. Реализация передачи обслуживания с использованием общей переменной

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

Допустим, у нас есть два потока - производитель и потребитель, и когда производитель устанавливает значение общей переменной, мы хотим сообщить об этом факте потоку-потребителю. Затем потребительский поток извлекает значение из общей переменной.

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

Мы определим переменнуюsharedState иCountDownLatch, которые будут использоваться для координации обработки:

ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(1);

Производитель сохранит случайное целое число в переменнойsharedState и выполнит методcountDown() дляcountDownLatch,, сигнализируя потребителю, что он может получить значение изsharedState:

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    sharedState.set(producedElement);
    countDownLatch.countDown();
};

Потребитель будет ожидатьcountDownLatch, используя методawait(). Когда производитель сигнализирует, что переменная установлена, потребитель получит ее изsharedState:

Runnable consumer = () -> {
    try {
        countDownLatch.await();
        Integer consumedElement = sharedState.get();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

И последнее, но не менее важное: давайте начнем нашу программу:

executor.execute(producer);
executor.execute(consumer);

executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(countDownLatch.getCount(), 0);

Он выдаст следующий вывод:

Saving an element: -1507375353 to the exchange point
consumed an element: -1507375353 from the exchange point

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

4. Реализация передачи обслуживания с использованиемSynchronousQueue

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

Во-первых, мы определим очередь:

ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue queue = new SynchronousQueue<>();

Производитель вызовет методput(), который будет блокироваться, пока какой-либо другой поток не возьмет элемент из очереди:

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    try {
        queue.put(producedElement);
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Потребитель просто получит этот элемент с помощью методаtake():

Runnable consumer = () -> {
    try {
        Integer consumedElement = queue.take();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Далее мы запустим нашу программу:

executor.execute(producer);
executor.execute(consumer);

executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(queue.size(), 0);

Он выдаст следующий вывод:

Saving an element: 339626897 to the exchange point
consumed an element: 339626897 from the exchange point

Мы видим, чтоSynchronousQueue используется в качестве точки обмена между потоками, что намного лучше и понятнее, чем в предыдущем примере, в котором общее состояние использовалось вместе сCountDownLatch.

5. Заключение

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

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.