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
Оставить комментарий
Useful Articles:
New Articles: