“This is the 20th day of my participation in the First Challenge 2022. For details: First Challenge 2022.”


In the world of programming languages, there seems to be a constant comparison between languages, not to mention that PHP was the best language in the world in the old days. Recently, when I was poking around, I saw a number of articles saying that “Golang beats Java in performance “. As a Javaer who has written Java for several years, how can I tolerate this? So I read some articles comparing Golang and Java on the Internet. Among them, the poke in the Java pain point, which is also the one that Golang is promoted to the sky, is the support for multi-threaded concurrency. Let’s take a look at the description:

Go from native language level support concurrent, and simple to use, the language of the concurrent threads based on lightweight Goroutine, create the cost is low, a single Go application can take full advantage of multi-core CPU, writing high concurrency server software is simple, performance is good, in many cases do not need to consider locking mechanism, and the resulting problems.

See this, my heart instantly cool big half, really is the heart of every word. Although the JUC package in Java has helped us to package a lot of concurrency tools, in a high-concurrency environment we have to consider the use of various locks, server performance bottlenecks, traffic limiting fuses, and many other issues.

Back to go, what exactly is this goroutine? In fact, the lightweight thread Goroutine can also be called a coroutine. Thanks to the scheduler in GO and the GMP model, the GO program intelligently allocates the tasks in the Goroutine to each CPU.

Well, in fact, the above said this paragraph I do not understand, are to write go friends to consult, in short, is go concurrency is very good. But that’s beside the point. Today we’re going to talk about how to use coroutines in Java.

What is a coroutine?

We know that switching between blocked and runnable threads, as well as context switching between threads, can take a toll on performance. To solve these problems, the concept of coroutine was introduced, just as multiple threads are allowed to exist in a process, and multiple coroutines can exist in a thread.

So what are the benefits of using coroutines?

First, the execution efficiency is high. Thread switching is performed by the operating system kernel and consumes a lot of resources. However, coroutines are controlled by programs and executed in user mode, so there is no need to switch from user mode to kernel mode. We can also understand that coroutines are a scheduling mode in which processes schedule tasks by themselves, so the switching overhead between coroutines is much less than thread switching.

Second, save resources. Because coroutines essentially reuse a single thread through time – sharing, they can save some resources.

Similar to the five state transitions of threads, there are state transitions between coroutines. The following diagram shows the flow of tasks within the coroutine scheduler.

Taken together, Java is vulnerable to multithreaded concurrency compared to go, which supports coroutines native to Java. However, while coroutines cannot be used directly in the official Java JDK, there are other open source frameworks that implement coroutines by dynamically modifying bytecodes, such as Quasar, which we will study next.

Quasar use

Quasar is an open source Java coroutine framework. It uses Java Instrument technology to modify the bytecode, so that the JVM stack frame can be saved and restored before and after the method is suspended. The bytecode position executed inside the method can be recorded by adding the state machine, and the latest position can be directly jumped to in the next recovery execution.

The Quasar project was last updated in 2018 at version 0.8.0, but I reported an error when using this version directly:

This class file was compiled using a older JDK, so you won’t be able to run it on older JDK versions. Here, the major version 54 corresponds to JDK10, while I use JDK8, but I have to downgrade to try the lower version, and sure enough 0.7.10 can be used:

<dependency>
    <groupId>co.paralleluniverse</groupId>
    <artifactId>quasar-core</artifactId>
    <version>0.7.10</version>
</dependency>
Copy the code

Now that we’re ready, let’s write a few examples to get a feel for the power of coroutines.

1. Running time

Let’s simulate a simple scenario. Suppose we have a task with an average execution time of 1 second and test how long it takes to execute 10,000 times concurrently using threads and coroutines.

* If the thread is open, go to the Executors thread pool:

public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch=new CountDownLatch(10000);
    long start = System.currentTimeMillis();
    ExecutorService executor= Executors.newCachedThreadPool();
    for (int i = 0; i < 10000; i++) {
        executor.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    long end = System.currentTimeMillis();
    System.out.println("Thread use:"+(end-start)+" ms");
}
Copy the code

To view the running time:

Ok, now let’s do the same thing with the Quasar coroutine. What we want to use here is Fiber in Quasar, which can be translated as coroutine or Fiber. Types of creating Fiber can be divided into the following two categories:

public Fiber(String name, FiberScheduler scheduler, int stackSize, SuspendableRunnable target);
public Fiber(String name, FiberScheduler scheduler, int stackSize, SuspendableCallable<V> target);
Copy the code

In Fiber, you can run SuspendableRunnable with no return value or SuspendableCallable with return value. Name is the name of the coroutine, scheduler is the scheduler, and FiberForkJoiner is used by default. StackSize specifies the stack size used to store fiber call stack information.

In the code below, the coroutine sleeps using the fiber.sleep () method, much like thread.sleep ().

public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch=new CountDownLatch(10000);
    long start = System.currentTimeMillis();

    for (int i = 0; i < 10000; i++) {
        new Fiber<>(new SuspendableRunnable(){
            @Override
            public Integer run(a) throws SuspendExecution, InterruptedException {
                Fiber.sleep(1000);
                countDownLatch.countDown();
            }
        }).start();
    }

    countDownLatch.await();
    long end = System.currentTimeMillis();
    System.out.println("Fiber use:"+(end-start)+" ms");
}
Copy the code

Run directly with a warning:

QUASAR WARNING: Quasar Java Agent isn't running. If you're using another instrumentation method you can ignore this message; otherwise, please refer to the Getting Started section in the Quasar documentation.
Copy the code

Remember that Quasar works based on Java Instrument technology, so you need to add an Agent to it. Find the jar package already stored in the local Maven repository and add the parameter to VM Options:

-javaagent:E:\Apache\maven-repository\co\paralleluniverse\quasar-core\0.7.10\quasar-core-0.7.10.jar
Copy the code

There is no warning this time, check the running time:

Running time is a little more than half of what it would be with a thread pool, which really makes your program much less efficient.

2. Memory usage

After testing the run time, let’s test the comparison of the run memory footprint. Try to start one million threads locally with the following code:

public static void main(String[] args) {
    for (int i = 0; i < 1000000; i++) {
        new Thread(() -> {
            try {
                Thread.sleep(100000);
            } catch(InterruptedException e) { e.printStackTrace(); } }).start(); }}Copy the code

I was expecting an OutOfMemoryError, but instead my computer froze… Not once, but several times, it ended up being stuck and had to restart the computer. Ok, I’m going to give up, so let’s try starting a million Fiber coroutines.

public static void main(String[] args) throws Exception {
    CountDownLatch countDownLatch=new CountDownLatch(10000);
    for (int i = 0; i < 1000000; i++) {
        int finalI = i;
        new Fiber<>((SuspendableCallable<Integer>)()->{
            Fiber.sleep(100000);
            countDownLatch.countDown();
            return finalI;
        }).start();
    }
    countDownLatch.await();
    System.out.println("end");
}
Copy the code

The program ends up executing normally, using much less memory than threads. I purposely left each coroutine long so that we could use Java VisualVM to see memory usage during the run:

As you can see, Fiber uses a little more than 1 gigabyte of memory, averaging over 1 million coroutines, which means that each Fiber takes up about 1Kb of memory, which is very lightweight compared to Thread.

As you can see from the above picture, there are a lot of ForkJoinpools running. What does it do? The Quasar scheduler uses one or more forkJoinpools to schedule Fiber.

3. Principle and application

For Quasar, the framework scans the code at compile time. If the method has the @Suspendable annotation, or throws the SuspendExecution, or specifies the method in the meta-INF/Suspendables configuration file, The Quasar would then modify the generated bytecode, inserting some bytecode before and after the Park suspend method.

The bytecode is recorded as coroutines execution status, such as related to the local variables and the operand stack, and then by throwing an exception to the CPU control from the current coroutines pay back to the controller, the controller can be scheduling another collaborators processes running again, and by inserting the bytecode before resuming the execution of the current coroutines state, make the program to continue normal execution.

Looking back at the SuspendableRunnable and SuspendableCallable examples, the SuspendExecution thrown on the run method is not really an exception, but a declaration that identifies the suspended method and is not thrown in the actual run. When we create a Fiber and call other methods in it, if we want the Quasar scheduler to intervene, we must layer upon layer throw the exception or add annotations at use.

Take a look at a simple code writing example:

public void request(a){
    new Fiber<>(new SuspendableRunnable() {
        @Override
        public void run(a) throws SuspendExecution, InterruptedException {
            String content = sendRequest();
            System.out.println(content);
        }
    }).start();
}

private String sendRequest(a) throws SuspendExecution {
    return realSendRequest();
}

private String realSendRequest(a) throws SuspendExecution{
    HttpResponse response = HttpRequest.get("http://127.0.0.1:6879/name").execute();
    String content = response.body();
    return content;
}
Copy the code

Note that if an Exception has already been caught by try/catch inside the method, you should also manually raise the SuspendExecution Exception again.

conclusion

This paper introduces the simple use of Quasar framework, its specific implementation principle is more complex, temporarily not discussed here, the following plan to separate out for analysis. In addition, there are a number of other frameworks that have integrated Quasar, such as the Comsat project under the Parallel Universe, which can provide HTTP and DB access and other functions.

The only way to use coroutine in Java is with third-party frameworks like this, but don’t lose heart. In OpenJDK 16, a Project called Project Loom, which you can see on the OpenJDK Wiki, uses Fiber lightweight user-mode threads. A radical change to multithreading at the JVM level, using a new programming model, enables concurrency of lightweight threads to be suitable for high-throughput business scenarios.

Quasar and Loom documents are listed below for those who are interested in them.

Quasar git:github.com/puniverse/q…

Quasar API: docs. Paralleluniverse. Co/Quasar/Java…

Its Wiki:wiki.openjdk.java.net/display/loo…

The last

If you feel helpful, you can click a “like” ah, thank you very much ~

Nongcanshang, an interesting, in-depth and direct public account that loves sharing, will talk to you about technology. Welcome to Hydra as a “like” friend.