В этой статье вы узнаете о некоторых полезных функциях Java, о которых вы, вероятно, не слышали. 

Это мой личный список функций, использованных мной недавно или с которыми я столкнулся при чтении статей о Java. 

Я сосредоточусь не на языковых аспектах, а на API. Я уже опубликовал все примеры, относящиеся к этой статье, в Твиттере в форме, показанной ниже. Вы также можете найти их в моей учетной записи Twitter или просто под хэштегом #java.

Вам нравится Java и вы хотите быть в курсе последних функций? Вы также можете прочитать мою статью о новых функциях появившихся после Java 8. Ниже вы можете найти список из восьми неизвестных, но полезных функций, описанных в этой статье. 

  1. Delay Queue

  2. Время суток в Time Format

  3. Stamped Lock

  4. Параллельные аккумуляторы

  5. Шестнадцатеричный формат

  6. Бинарный поиск в массивах

  7. Bit Set

  8. Phaser

Давайте начнем!

1. Delay Queue

Как вы знаете, в Java доступно множество типов коллекций. Но вы слышали об DelayQueue

Это особый тип коллекции Java, которая позволяет нам сортировать элементы по времени их задержки. 

Если честно, это очень интересный класс. Хотя класс DelayQueue является членом коллекций Java, он принадлежит пакету java.util.concurrent. Он реализует интерфейс BlockingQueue. Элементы могут быть взяты из очереди только в том случае, если их время истекло.

Чтобы использовать его, во-первых, ваш класс должен реализовать метод getDelay из интерфейса Delayed. Это не обязательно должен быть класс - вы также можете использовать Java Record.

public record DelayedEvent(long startTime, String msg) implements Delayed {

    public long getDelay(TimeUnit unit) {
        long diff = startTime - System.currentTimeMillis();
        return unit.convert(diff, TimeUnit.MILLISECONDS);
    }

    public int compareTo(Delayed o) {
        return (int) (this.startTime - ((DelayedEvent) o).startTime);
    }

}

Допустим, мы хотим задержать элемент на 10 секунд. Нам просто нужно установить текущее время, увеличенное на 10 секунд для нашего класса DelayedEvent.

final DelayQueue<DelayedEvent> delayQueue = new DelayQueue<>();
final long timeFirst = System.currentTimeMillis() + 10000;
delayQueue.offer(new DelayedEvent(timeFirst, "1"));
log.info("Done");
log.info(delayQueue.take().msg());

Какой вывод из кода выше? Посмотрим.

2. Время суток в Time Format

Хорошо, может быть, это не будет одной из функций Java, очень полезных для большинства из вас. 

Но, честно говоря, у меня есть слабость к этой функции ...

В любом случае Java 8 значительно улучшила API обработки времени. Начиная с этой версии Java, в большинстве случаев вам, вероятно, не придется использовать какую-либо дополнительную библиотеку, такую ​​как Joda Time. 

Можете ли вы представить себе, что начиная с Java 16 вы можете даже выражать время суток, например, «утром» или «днем», используя стандартный форматер?  Для этого есть новый шаблон формата B.

String s = DateTimeFormatter
  .ofPattern("B")
  .format(LocalDateTime.now());
System.out.println(s);

Вот мой результат. Но, конечно, ваш результат зависит от вашего времени суток.

Ок. Погодите ... Теперь вы, наверное, задаетесь вопросом, почему он называется B. Не спрашивайте 

На самом деле, это не самое интуитивно понятное название для этого типа формата. Но, возможно, следующая таблица разрешает все сомнения. Это фрагмент шаблонных букв и символов, обрабатываемых DateTimeFormatter

Я предполагаю, что B была первой свободной буквой. Но, конечно, я мог ошибаться.

3. Stamped Lock

На мой взгляд, Java Concurrent - один из самых интересных пакетов Java. И в то же время один из менее известных у разработчиков, особенно если они работают в основном с WEB фреймворками. 

Кто из вас когда-либо использовал блокировки в Java? Блокировка Lock - более гибкий механизм синхронизации потоков, чем synchronized

Начиная с Java 8, вы можете использовать новый вид блокировки, называемый StampedLock, являющийся альтернативой использованию ReadWriteLock. Она допускает оптимистичную блокировку операций чтения. Кроме того, она имеет лучшую производительность, чем ReentrantReadWriteLock.

Допустим, у нас есть два потока. Первый из них обновляет баланс, а второй считывает текущее значение баланса. Чтобы обновить баланс, нам, конечно, нужно сначала прочитать его текущее значение. Здесь нам нужна какая-то синхронизация, если первый поток выполняется несколько раз одновременно. Второй поток просто иллюстрирует, как использовать оптимистичную блокировку для операции чтения.

StampedLock lock = new StampedLock();
Balance b = new Balance(10000);
Runnable w = () -> {
   long stamp = lock.writeLock();
   b.setAmount(b.getAmount() + 1000);
   System.out.println("Write: " + b.getAmount());
   lock.unlockWrite(stamp);
};
Runnable r = () -> {
   long stamp = lock.tryOptimisticRead();
   if (!lock.validate(stamp)) {
      stamp = lock.readLock();
      try {
         System.out.println("Read: " + b.getAmount());
      } finally {
         lock.unlockRead(stamp);
      }
   } else {
      System.out.println("Optimistic read fails");
   }
};

Теперь давайте просто протестируем это, запустив оба потока одновременно по 50 раз. Должно сработать как положено - итоговое значение баланса 60000.

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 50; i++) {
   executor.submit(w);
   executor.submit(r);
}

4. Параллельные аккумуляторы

Блокировки - не единственная интересная функция в пакете Java Concurrent. Другой называется параллельными аккумуляторами. Существуют также параллельные сумматоры, но они имеют довольно похожую функциональность. LongAccumulator обновляет значение (есть также DoubleAccumulator), используя предоставленную функцию. Это позволяет нам реализовать алгоритм без блокировок в ряде сценариев. Обычно это предпочтительнее чем AtomicLong, когда несколько потоков обновляют общее значение.

Посмотрим, как это работает. 

Для того чтобы создать аккумулятор, вам нужно указать в конструкторе два аргумента. Первый из них - это функция, используемая для вычисления результата аккумулятора. Обычно это метод sum. Второй параметр указывает начальное значение нашего аккумулятора.

Теперь давайте создадим LongAccumulator с начальным значением 10000а затем вызовем метод accumulate() из нескольких потоков. Каков конечный результат? Если задуматься, мы сделали, то же самое, что и в предыдущем разделе. Но на этот раз без использования блокировки.

LongAccumulator balance = new LongAccumulator(Long::sum, 10000L);
Runnable w = () -> balance.accumulate(1000L);

ExecutorService executor = Executors.newFixedThreadPool(50);
for (int i = 0; i < 50; i++) {
   executor.submit(w);
}

executor.shutdown();
if (executor.awaitTermination(1000L, TimeUnit.MILLISECONDS))
   System.out.println("Balance: " + balance.get());
assert balance.get() == 60000L;

5. Шестнадцатеричный формат

За этим нет большой истории. Иногда нам нужно преобразовать строки в шестнадцатеричный формат, байты или символы. Начиная с Java 17 вы можете использовать класс HexFormat. Просто создайте экземпляр HexFormat, а затем вы можете отформатировать, например, входную таблицу byte в шестнадцатеричную строку. Вы также можете, например, преобразовать входную шестнадцатеричную строку в таблицу байтов, как показано ниже.

HexFormat format = HexFormat.of();

byte[] input = new byte[] {127, 0, -50, 105};
String hex = format.formatHex(input);
System.out.println(hex);

byte[] output = format.parseHex(hex);
assert Arrays.compare(input, output) == 0;

6. Бинарный поиск в массивах

Допустим, мы хотим вставить новый элемент в отсортированную таблицу. Arrays.binarySearch() возвращает индекс ключа поиска, если он содержится в таблице. В противном случае она возвращает точку вставки, которую мы можем использовать для подсчета индекса для нового ключа: -(insertion point)-1. Более того, метод binarySearch является самым простым и эффективным методом поиска элемента в отсортированном массиве в Java.

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

int[] t = new int[] {1, 2, 4, 5};
int x = Arrays.binarySearch(t, 3);

assert ~x == 2;

7. Bit Set

Что, если нам нужно выполнить какие-то операции с массивами битов? Вы будете использовать для этого boolean[]

Для этого есть более эффективный с точки зрения использования памяти метод. 

Это класс BitSet, позволяющий нам хранить массивы битов и манипулировать ими. По сравнению с boolean[] он требует в 8 раз меньше памяти. Мы можем выполнять логические операции над массивами, такими как, например and, or, xor.

Допустим, у нас есть два входных массива битов. Мы хотим провести на них операцию xor

Уточню, операция xor, возвращает только те элементы, которые имеются только в одном массиве, но не в другом. Для этого нам нужно создать два экземпляра BitSet и вставить туда элементы, как показано ниже. Наконец, вы должны вызвать метод xor в одном из BitSet объектов, указав в качестве аргумента второй BitSet объект.

BitSet bs1 = new BitSet();
bs1.set(0);
bs1.set(2);
bs1.set(4);
System.out.println("bs1 : " + bs1);

BitSet bs2 = new BitSet();
bs2.set(1);
bs2.set(2);
bs2.set(3);
System.out.println("bs2 : " + bs2);

bs2.xor(bs1);
System.out.println("xor: " + bs2);

Вот результат после выполнения кода, показанный выше.

8. Phaser

Наконец, последняя в этой статье интересная функция Java. Как и некоторые другие примеры здесь, она также класс пакета Java Concurrent. Она называется Phaser. Он очень похожа на более известную CountDownLatch

Однако он предоставляет некоторые дополнительные функции. Он позволяет нам установить динамическое количество потоков, которые должны ждать перед продолжением выполнения. 

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

В следующем примере мы устанавливаем барьер в 50 потоков до перехода к следующей фазе выполнения. 

Затем мы создаем поток, который вызывает метод arriveAndAwaitAdvance() в экземпляре класса Phaser. Он блокирует поток до тех пор, пока все 50 потоков не дойдут до барьера. Затем он переходит к phase-1 и вызывает метод arriveAndAwaitAdvance().

Phaser phaser = new Phaser(50);
Runnable r = () -> {
   System.out.println("phase-0");
   phaser.arriveAndAwaitAdvance();
   System.out.println("phase-1");
   phaser.arriveAndAwaitAdvance();
   System.out.println("phase-2");
   phaser.arriveAndDeregister();
};

ExecutorService executor = Executors.newFixedThreadPool(50);
for (int i = 0; i < 50; i++) {
   executor.submit(r);
}

Вот результат после выполнения кода, показанного выше.

Комментарии (7)


  1. qasta
    11.01.2022 16:11
    +6

    Спасибо за интересную подборку. В очередной раз убеждаюсь - век живи, век учись. Про 4 пункта из 8 не знал, хотя с потоками в своё время ну очень плотно работал. Ну и статья про старое API, а не про новый синтаксис - как глоток свежего воздуха.


  1. vasyakolobok77
    11.01.2022 21:23
    +1

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

    Не такие уж это и "неизвестные" механизмы. Причина, по которой они редко встречаются в коде, думаю, многим понятна – это очень узкоспециализированные механизмы, некоторые и вовсе бесполезные в прикладной разработке.

    StampedLock, Concurrent Accumulators, Phaser – вещи сугубо для сильноконкурентных приложений, потому как для обычных бизнес-приложений есть более понятные механизмы и выгоды от StampedLock вы не увидите. И вот, если вы пишите такие сильноконкуретные вещи, то скорее всего вы уже итак глубоко знакомы java.util.concurrent.

    TimeFormat "B" показывающий "in the morning" такое себе, чаще всего (и правильнее) форматирование даты/времени происходит на клиенте, а для machine-to-machine используется бинарный формат.

    HexFormat, что появился в 17 java. Ну наконец-то, ждали его с 95 года, и как же мы без него жили-то? sarcasm. :)

    Для DelayQueue я даже не могу придумать реальную бизнес-задачу.

    О BinarySearch для отсортированных массивов думаю знает любой джун.

    BitSet тоже всем известная вещь. И важно отметить, что он непотокобезопасен. В отличие от того же boolean[].


    1. tsypanov
      12.01.2022 18:45

      И важно отметить, что он непотокобезопасен. В отличие от того же boolean[].

      Справедливости ради, сам по себе голый boolean[] является потокобезопасным только при чтении из него.


    1. Tuchnyak
      13.01.2022 10:16

      DelayQueue использую в заглушке для тестовых прогонов, чтобы имитировать задержку возвращения ответа)

      Для бизнеса, например, можно ставить напоминания. Понятно, что можно и другими способами сделать)


  1. tsypanov
    12.01.2022 18:41
    +1

    Обычно это предпочтительнее чем AtomicLong, когда несколько потоков обновляют общее значение.

    Здесь стоило бы вкратце указать за счёт чего именно LongAccumulator выигрывает у простого атомика при одновременном доступе из многих потоков, а также упомянуть LongAdder, тем более что в большинстве случаев его достаточно.


    1. val6852 Автор
      13.01.2022 12:36

      В доке https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/LongAccumulator.html написано:

      This class is usually preferable to AtomicLong when multiple threads update a common value that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.

      Кратко: эти два класса имеют схожие характеристики. Но при высокой конкуренции ожидаемая пропускная способность этого класса значительно выше.


      1. tsypanov
        13.01.2022 13:06

        В своём вопросе я имел ввиду краткое описание механизма, обеспечивающего лучшую производительность (здесь - выравнивание по кэш-линии). И LongAccumulator во многих случаях избыточен, вместо него можно взять LongAdder, реализующий то же выравнивание. Понятно, что эти вопросы скорее к Петру Миньковскому, автору оригинала )