Начиная с 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-язык.