Breaking down: Rate-limiter, non-blocking operations, scheduler Go vs Java | Concurrency part 4

This article is dedicated to understanding the principles of working with concurrency and synchronization in Go and Java. We will look at key approaches such as rate-limiter, non-blocking operations, and task scheduling, compare their implementation and philosophy in both languages. This will help a Java developer quickly master Go and, conversely, a Go developer understand Java.

Rate‑Limiter

Rate‑limiter allows you to limit the frequency of operations to avoid overloading the system. In Go, we often use channels and timers, in Java — 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 task once every second
        scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);

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

Non‑blocking operations / Non-blocking operations

Non-blocking operations allow actions to be performed in parallel without waiting for other threads to complete. In Go, this is easily achieved through goroutines and channels, in Java — through CompletableFuture or non-blocking data structures.

// Go: неблокирующее чтение из канала
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: неблокирующее получение из очереди
import java.util.concurrent.*;

public class NonBlockingExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        Integer val = queue.poll(); // poll не блокирует
        if (val != null) {
            System.out.println("Received " + val);
        } else {
            System.out.println("No value available, continue working");
        }
    }
}
  
Tip: non-blocking operations increase system responsiveness. In Go, this is naturally done 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 ExecutorService.

// Go: simple demonstration of goroutines and scheduler
package main

import (
    "fmt"
    "runtime"
)

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

func main() {
    runtime.GOMAXPROCS(2) // use 2 system threads
    for i := 0; i < 4; i++ {
        go worker(i)
    }

    // wait for goroutines to finish (for simplicity)
    var input string
    fmt.Scanln(&input)
}
  
// Java: executing multiple tasks through 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 the scheduler, allowing for thousands of tasks to be created without overload. In Java, use ExecutorService and thread pools for task management, avoiding the creation of too many threads.

Comparison table: Go vs Java on concurrency

Topic Go Java Comment
Rate‑Limiter Channels + Ticker ScheduledExecutorService, Guava Go makes it easy to implement a simple limit, Java often uses ready-made libraries
Non-blocking select with default, goroutines poll(), CompletableFuture, asynchronous APIs In Go, natural non-blocking through channels, in Java you need to use specific structures or APIs
Scheduler GMP (goroutine → P → M → OS thread) ExecutorService and OS thread pool Go can create thousands of lightweight goroutines, Java is better at managing the number of threads through a pool

Example Worker Pool 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.*;

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();
    }
}
  

Output / Results

Concurrency in Go and Java is implemented according to different philosophies:

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

Practical tips:

  • For simple tasks and thousands of operations, use goroutines in Go.
  • For controlling 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 - with simple tickers, Java - with libraries.

Understanding these differences allows for quick adaptation of experience from one language to another, leveraging the strengths of each model.

Previous articles in the series: Basic types and data structures in Go vs Java | Generics, Reflection, and advanced types


🌐 На русском
Total Likes:0

Оставить комментарий

My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Java v25: Choosing the Right Multithreading for Any Task
Introduction The Java world is rapidly evolving, and with each version, new tools are emerging for effectively working with multithreading, collections, and asynchrony. Java 25 brings powerful feature...
Slice internals in Go ↔ Java: from header to hidden allocations
Slice in Go is one of those structures that looks simple, but under the hood behaves like a little clever beast. If you are a Java developer, you might think: "well, this is just an ArrayList." And th...
Go vs Java - comparison of memory models: happens-before, visibility, reorder, synchronization events, write/read barriers
Memory model is a layer between the program and the processor. Modern CPUs aggressively optimize execution: instructions may be reordered, data may be stored in core caches, and operations may be perf...

New Articles:

Concurrency is not about “starting many threads”. It’s about agreements between them. Imagine a restaurant kitchen: — cooks (threads / goroutines) — orders (tasks) — and the main question: how do th...
When HashMap starts killing production: the engineering story of ConcurrentHashMap
Imagine a typical production service. 32 CPU hundreds of threads configuration / session / rate limits cache tens of thousands of operations per second And somewhere inside — a regular Map. At first...
Zero Allocation in Java: what it is and why it matters
Zero Allocation — is an approach to writing code in which no unnecessary objects are created in heap memory during runtime. The main idea: fewer objects → less GC → higher stability and performance. ...
Fullscreen image