Implementing Go Concurrency Features in Java Part 1: Implementing the Go Keyword and Wait Groups

William Yin
6 min readJul 5, 2024

--

This article is the first in a series of articles documenting my process implementing Go concurrency features in Java. I was motivated to implement Go concurrency features when I noticed how much more fun and painless it is to write concurrent programs in Go compared to Java. In this article I will document how I implemented Go’s go keyword and WaitGroup in Java.

Overview

In Go, running a function on a new thread is as simple as putting the go keyword in front of the function call. It is important to note however that the thread started by the go keyword is the equivalent of daemon thread in Java. This means that if the program‘s main() function completes, it will not wait for the thread to complete its task before exiting. To get around this, we can use Go’s WaitGroup which contains a counter that can be added to and subtracted from and any thread that calls the wait group’s Wait() function will wait until the counter reaches zero. The documentation for WaitGroup can be found here. To demonstrate how much more elegant multithreaded programming is in Go compared to Java, let’s compare how we would write a program that counts to a certain number on a separate thread in both languages.

In Go, the program would look like this.

// Sample Go program that starts a goroutine which counts to 10 and then returns.
package main

import (
"fmt"
"sync"
)

func count(n int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= n; i++ {
fmt.Println(i)
}
}

func main() {
var wg sync.WaitGroup
wg.Add(1)
go count(10, &wg)
wg.Wait()
}

In Java, the same program would look like this.

class CountJava {

public static void main(String[] args) {
Thread t = new Thread(new Counter(10));
t.start();
}

private static class Counter implements Runnable {

private final int n;

public Counter(int n) {
this.n = n;
}

@Override
public void run() {
for (int i = 1; i <= n; i++) {
System.out.println(i);
}
}
}
}

Compared to Go, in Java we had to create a class that implements the Runnable interface, create a constructor for that class, create a thread object, and start the thread. It would be much more convenient if we could simply pass a method into a go method that handles all the setup for us and use a WaitGroup to wait until the method completes. By the end of this article, we will be able to do just that and our Java program will look like the following.

import io.javago.sync.WaitGroup;

import static io.javago.Go.go;

class Count {

public static void main(String[] args) {
final WaitGroup wg = new WaitGroup();
wg.add(1);
go(() -> count(10, wg));
wg.await();
}

private static void count(int n, WaitGroup wg) {
try (wg) {
for (int i = 1; i <= n; i++) {
System.out.println(i);
}
}
}
}

Implementing the Go Keyword

The go keyword will be a static method in a Go class that takes in a Runnable and sends it to a ExecutorService to be executed on a virtual thread. To ensure the ExecutorService is automatically shutdown when the JVM exits, we will add a shutdown hook in a static block. The code for implementing the go keyword is given below.

package io.javago;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
* The {@code Go} class implements Go's {@code go} statement.
* It provides a simple way to execute tasks asynchronously using a virtual thread per task executor.
* It initializes a thread pool and ensures that it is properly shut down when the JVM exits.
*
* <p>This class is intended to be used for running {@link Runnable} tasks in a multi-threaded environment.</p>
*
* @see java.util.concurrent.ExecutorService
* @see java.util.concurrent.Executors
*/
public class Go {

/**
* The thread pool used for executing tasks.
* It is initialized as a virtual thread per task executor.
*/
private static final ExecutorService threadPool;

// Static block to add a shutdown hook to ensure the thread pool is properly shut down when the JVM exits.
static {
ThreadFactory threadFactory = Thread.ofVirtual().name("go-thread-", 0).factory();
threadPool = Executors.newCachedThreadPool(threadFactory);
Runtime.getRuntime().addShutdownHook(new Thread(threadPool::shutdown));
}

private Go() {}

/**
* Executes the given task asynchronously using the thread pool.
* Used to recreate Go's {@code go} keyword in Java.
*
* @param r the task to be executed
* @throws NullPointerException if the task is null
*/
public static void go(Runnable r) {
threadPool.execute(r);
}
}

Keep in mind that this code makes use of virtual threads which were introduced in Java 21. Virtual threads differ from regular Java threads in that they do not correspond to a platform thread meaning we can create much more of them.

Implementing Wait Groups

You might think that wait groups are the same as Java’s CountDownLatch which also waits until a counter reaches zero. However, wait groups differ from count down latches in that you can count up on wait group whereas you can only ever count down on a count down latch. You might then think that using Java’s Semaphore class is a good way to implement WaitGroup as you can use the number of permits in a Semaphore as the counter and “count up” by releasing permits. However, this is not a good approach either as there is no easy way to wait on a semaphore until all of its permits are exhausted. Thus, the most straightforward way of implementing wait groups in Java is by creating a simple class with a counter field that can be added to and subtracted from. The object should have a method that causes the thread calling it to wait until counter reaches zero. All methods in the class should be marked as synchronized to make the object thread-safe. The code for implementing wait groups in Java is given below.

package io.javago.sync;

/**
* The {@code WaitGroup} class implements Go's {@code sync.WaitGroup}.
* A synchronization aid that allows one or more threads to wait until a set of operations being performed in other
* threads completes.
* It implements the {@link AutoCloseable} interface and allows a try-with-resources statement to automatically decrease
* its count by one.
*/
public class WaitGroup implements AutoCloseable {
private int count;

/**
* Constructs a new {@code WaitGroup} with an initial count of zero.
*/
public WaitGroup() {
this.count = 0;
}

/**
* Increments the count of this wait group by the specified amount.
*
* @param amount the amount by which to increment the count
*/
public synchronized void add(int amount) {
count += amount;
}

/**
* Decrements the count of this wait group by one.
* If the count reaches zero, all waiting threads are notified.
* Equivalent to {@link #close()}.
*
* @throws IllegalStateException if the wait group has already reached zero
*/
public synchronized void done() {
if (count == 0) {
throw new IllegalStateException("WaitGroup has already reached zero");
}

count--;
if (count == 0) {
this.notifyAll();
}
}

/**
* Causes the current thread to wait until the count of this wait group reaches zero.
* If the current count is zero, this method returns immediately.
*/
public synchronized void await() {
if (count > 0) {
try {
this.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

/**
* Decrements the count of this wait group by one.
* If the count reaches zero, all waiting threads are notified.
* Equivalent to {@link #done()}.
*
* @throws IllegalStateException if the wait group has already reached zero
*/
@Override
public synchronized void close() {
done();
}
}

The WaitGroup class implements the AutoCloseable interface so that we can use it in a try-with-resources block since Java does not have an equivalent to Go’s defer keyword.

Conclusion

In this article we implemented the go keyword and wait groups. Writing multithreaded programs in Java is now much simpler and easier. In the next article we will implement Go’s channels which will allow us to easily send and receive data between threads.

For the next article in this series, click here.

To view the official web page for this library, click here.

--

--