Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4

This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We'll cover key approaches such as rate-limiter, non-blocking operations, and task scheduling, and compare their implementation and philosophy in the two languages. This will help a Java developer quickly learn Go and, conversely, a gopher understand Java.

Rate-Limiter

Rate-limiter allows you to limit the execution rate of operations to avoid overloading the system. In Go, we often use channels and timers, while in Java, we use the java.util.concurrent library or third-party solutions like Guava.


// Go: Simple rate limiter using a channel and ticker
package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(time.Second) // 1 operation per second
    defer ticker.Stop()

    for i := 0; i < 5; i++ {
        <-ticker.C
        fmt.Println("Operation", i)
    }
}

// Java: Rate limiter using ScheduledExecutorService
import java.util.concurrent.*;

public class RateLimiterExample {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        Runnable task = () -> System.out.println("Operation executed");

        // schedule the task every 1 second
        scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);

        // execute 5 operations and then close the scheduler
        Thread.sleep(5000);
        scheduler.shutdown();
    }
}
Tip: rate-limiter is useful for APIs, network requests, and operation rate limits. In Go, a simple channel with a ticker is sufficient; in Java, it's better to use ready-made solutions to avoid inventing complex timers manually.

Non-blocking operations

Non-blocking operations allow you to execute actions in parallel without waiting for other threads to complete. In Go, this is easily implemented using goroutines and channels; in Java, using CompletableFutures or non-blocking data structures.


// Go: Non-blocking reading from a channel
package main

import "fmt"

func main() {
    ch := make(chan int, 1)

    select {
    case val := <-ch:
        fmt.Println("Received", val)
    default:
        fmt.Println("No value available, continue working")
    }
}

// Java: Non-blocking receiving from a queue
import java.util.concurrent.*;

public class NonBlockingExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        Integer val = queue.poll(); // poll does not block 

        if (val != null) { 
            System.out.println("Received " + val); 
        } else { 
            System.out.println("No value available, continue working"); 
        } 
    }
} 
Tip: Non-blocking operations improve system responsiveness. In Go, this is naturally achieved through select with default; in Java, use poll, CompletableFuture, or asynchronous APIs.

Scheduler / Scheduling (GMP model)

Go uses the GMP (Goroutine-M-Processor) model for scheduling: lightweight goroutines are distributed across system threads via P-processors. In Java, the scheduler operates at the OS thread level, and task management is handled by the ExecutorService.


// Go: A simple demo of goroutines and the scheduler
package main

import (
    "fmt"
    "runtime"
)

func worker(id int) {
    fmt.Println("Worker", id, "started")
}

func main() {
    runtime.GOMAXPROCS(2) // Using 2 system threads

    for i := 0; i < 4; i++ {
        go worker(i)
    }

    // Wait for goroutines to complete (for simplicity)
    var input string
    fmt.Scanln(&input)
}

// Java: Executing Multiple Tasks via ExecutorService
import java.util.concurrent.*;

public class SchedulerExample { 
    public static void main(String[] args) throws InterruptedException { 
        ExecutorService executor = Executors.newFixedThreadPool(2); // 2 threads 

        for (int i = 0; i < 4; i++) { 
            final int id = i; 
            executor.submit(() -> System.out.println("Worker " + id + " started")); 
        } 

        executor.shutdown(); 
        executor.awaitTermination(1, TimeUnit.MINUTES); 
    }
}
Tip: In Go, goroutines are lightweight and managed by a scheduler, allowing you to create thousands of tasks without overloading them. In Java, use ExecutorService and thread pools to manage tasks, avoiding creating too many threads.

Summary Table: Go vs. Java by Concurrency

Topic Go Java Comment
Rate-Limiter Channels + Ticker ScheduledExecutorService, Guava Go makes it easy to implement a simple rate limit; Java often uses ready-made libraries
Non-blocking select with default, goroutines poll(), CompletableFuture, asynchronous API Go has natural non-blocking via channels, while Java requires using specific structures or APIs
Scheduler GMP (goroutine → P → M → OS thread) ExecutorService and the OS thread pool Go can create thousands of lightweight goroutines, while Java manages the number of threads better through the thread pool

Worker Pool Example in Go and Java


// Go: worker pool
package main

import "fmt"

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs { 
        fmt.Println("Worker", id, "processing job", j) 
        results <- j * 2 
    }
}

func main() { 
    jobs := make(chan int, 5) 
    results := make(chan int, 5) 

    for w := 1; w <= 2; w++ { 
        go worker(w, jobs, results) 
    } 

    for j := 1; j <= 5; j++ { 
        jobs <- j 
    } 
    close(jobs) 

    for a := 1; a <= 5; a++ { 
        fmt.Println("Result:", <-results) 
    }
}

// Java: worker pool
import java.util.concurrent.*;
import java.util.*;

public class WorkerPoolExample { 
    public static void main(String[] args) throws InterruptedException, ExecutionException { 
        ExecutorService executor = Executors.newFixedThreadPool(2); 
        BlockingQueue<Integer> jobs = new LinkedBlockingQueue<>(); 
        for (int j = 1; j <= 5; j++) jobs.add(j); 

        List<Future<Integer>> results = new ArrayList<>(); 
        for (int i = 0; i < 2; i++) { 
            results.add(executor.submit(() -> { 
                Integer job = jobs.poll(); 
                if (job != null) { 
                    System.out.println("Processing job " + job); 
                    return job * 2; 
                } 
                return null; 
            })); 
        } 

        for (Future<Integer> f : results) { 
            if (f.get() != null) System.out.println("Result: " + f.get()); 
        } 

        executor.shutdown(); 
    }
}

Conclusion / Summary

Concurrency in Go and Java is implemented using different philosophies:

  • Go: lightweight goroutines, channels, built-in GMP scheduler, convenient non-blocking constructs.
  • Java: heavy OS threads, ExecutorService, CompletableFuture, ready-made structures for rate-limiting and asynchrony.

Practical tips:

  • For simple tasks and thousands of operations, use goroutines in Go.
  • To control concurrency in Java, use thread pools and schedulers.
  • Non-blocking operations help maintain responsiveness; use select in Go and poll / CompletableFuture in Java.
  • Rate limiter is needed for APIs and external resources: Go for simple tickers, Java for libraries.

Understanding these differences allows you to quickly adapt your experience from one language to the other, leveraging the strengths of each model.


🌐 На русском
Total Likes:0
My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Go vs Java - memory model comparison: happens-before, visibility, reorder, synchronization events, write/read barriers
The memory model is a layer between the program and the processor. Modern CPUs aggressively optimize execution: instructions can be reordered, data can be stored in core caches, and operations can be ...
Asynchrony and Reactivity in Java: CompletableFuture, Flow, and Virtual Threads
In modern Java development, there are three main approaches to asynchrony and concurrency: CompletableFuture — for single asynchronous tasks. Flow / Reactive Streams — for data flows with backpressur...
Java Under the Microscope: Stack, Heap, and GC in Sample Code
Diagram - Java Memory Model - Heap / Non-Heap / Stack Heap (memory for objects) Creates objects using new. Young Generation: Eden + Survivor. Old Generation: Objects that have survived multiple G...

New Articles:

Generics, Reflection and Channels - Go vs Java | Types - Language
In this article we will analyze advanced type system features in Go: generics (type parameters), reflection, and channel types for concurrency. We will compare Go and Java approaches, so Java develope...
Let's look at: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection in Go vs Java | Testing, Debugging and Profiling
Series: Go for Java Developers — analysis of trace, profiling and testing In this article we will analyze tools and practices for testing, debugging and profiling in Go. For a Java developer this wil...
Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4
This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We ll cover key approaches such as rate-limiter, non-blocking operations, and task scheduli...
Fullscreen image