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.
Useful Articles:
New Articles: