Начиная с Java 19 нам доступны виртуальные потоки, которые отличаются от обычных, тем что умеют освобождать поток операционной системы во время блокирующих I/O операций. Для этого на уровне JVM был реализован механизм сохранения в хипе и восстановления из хипа стека вызова. Проще говоря, были реализованы полноценные корутины на уровне JVM.
И это небольшая революция, на которую мало кто обратил внимание. Само API для таких нативных корутин непубличное, доступно через класс jdk.internal.vm.Continuation, в котором есть методы yield() и run() для сохранения и восстановления стека вызова соответственно. Но получить доступ до него несложно, нужно лишь добавить пару аргументов в строку запуска JVM (либо воспользоваться инструментом, который позволяет обходить ограничения JPMS).
Поэтому представляю свою небольшую библиотеку для доступа к нативным корутинам на Java: https://github.com/Anamorphosee/loomoroutines.
У многих может возникнуть вопрос, где нам могут быть нужны корутины, кроме виртуальных потоков? Ответ: везде, где мы пишем асинхронный код на колбеках, его можно заменить на синхронный код на корутинах. Например, для GUI приложений, моя обертка позволяет написать вот так:
Java GUI App Example
import dev.reformator.loomoroutines.dispatcher.SwingDispatcher; import dev.reformator.loomoroutines.dispatcher.VirtualThreadsDispatcher; import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.regex.Pattern; import static dev.reformator.loomoroutines.dispatcher.DispatcherUtils.*; public class ExampleSwing { private static int pickingCatCounter = 0; private static final Pattern urlPattern = Pattern.compile("\"url\":\"([^\"]+)\""); public static void main(String[] args) { var frame = new JFrame("Cats"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); var panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); var button = new JButton("Pick a cat"); var imagePanel = new ImagePanel(); panel.add(button); panel.add(imagePanel); frame.add(panel); frame.setSize(1000, 500); frame.setVisible(true); button.addActionListener(e -> dispatch(SwingDispatcher.INSTANCE, () -> { pickingCatCounter++; if (pickingCatCounter % 2 == 0) { button.setText("Pick another cat"); return null; } else { button.setText("This one!"); var cachedPickingCatCounter = pickingCatCounter; try { while (true) { var bufferedImage = doIn(VirtualThreadsDispatcher.INSTANCE, ExampleSwing::loadCatImage); if (pickingCatCounter != cachedPickingCatCounter) { return null; } imagePanel.setImage(bufferedImage); delay(Duration.ofSeconds(1)); if (pickingCatCounter != cachedPickingCatCounter) { return null; } } } catch (Throwable ex) { if (pickingCatCounter == cachedPickingCatCounter) { ex.printStackTrace(); pickingCatCounter++; button.setText("Exception: " + ex.getMessage() + ". Try again?"); } return null; } } })); } private static BufferedImage loadCatImage() { String url; { String json; try (var stream = URI.create("https://api.thecatapi.com/v1/images/search").toURL().openStream()) { json = new String(stream.readAllBytes()); } catch (IOException ex) { throw new RuntimeException(ex); } var mather = urlPattern.matcher(json); if (!mather.find()) { throw new RuntimeException("cat url is not found in json '" + json + "'"); } url = mather.group(1); } try (var stream = URI.create(url).toURL().openStream()) { return ImageIO.read(stream); } catch (IOException ex) { throw new RuntimeException(ex); } } } class ImagePanel extends JPanel { private BufferedImage image = null; public void setImage(BufferedImage image) { this.image = image; repaint(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (image != null) { g.drawImage(image, 0, 0, null); } } }
Обратите внимание, что в примере нет ни одного колбека, код написан в синхронном стиле, как будто все операции производятся в UI потоке. На самом же деле блокирующие операции (загрузка изображение и ожидание) производятся в другом потоке и не блокируют UI.
Хорошо, но зачем нам нативные которутины, когда есть Kotlin, в котором они уже давно реализованы и не требуют поддержки со стороны рантайма? Тут я могу отметить, что Kotlin-корутины реализованы слишком оптимизировано и из-за этого имеются сложности с их отладкой (в них после восстановления обрезается стек вызова). Кроме того, Kotlin-корутины обязывают использовать Kotlin, для Loom-корутин же можно использовать Java, Scala, Kotlin, Groovy или любой другой JVM-язык.
