Разбираем: Rate‑limiter, non‑blocking operations, scheduler Go vs Java | Concurrency часть 4

Эта статья посвящена пониманию принципов работы с конкурентностью и синхронизацией в Go и Java. Мы рассмотрим ключевые подходы, такие как rate‑limiter, неблокирующие операции и планирование задач, сравним их реализацию и философию в двух языках. Это поможет Java-разработчику быстро освоить Go и, наоборот, гоферу понять Java.

Rate‑Limiter

Rate‑limiter позволяет ограничивать частоту выполнения операций, чтобы не перегружать систему. В Go мы часто используем каналы и таймеры, в Java — библиотеку java.util.concurrent или сторонние решения вроде Guava.

// Go: простой rate limiter с использованием канала и ticker
package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(time.Second) // 1 операция в секунду
    defer ticker.Stop()
    for i := 0; i < 5; i++ {
        <-ticker.C
        fmt.Println("Operation", i)
    }
}
  
// Java: rate limiter с использованием 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");

        // планируем задачу раз в 1 секунду
        scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);

        // выполняем 5 операций и затем закрываем scheduler
        Thread.sleep(5000);
        scheduler.shutdown();
    }
}
  
Совет: rate-limiter удобен для API, сетевых запросов и ограничений на количество операций. В Go достаточно простого канала с тикером, в Java лучше использовать готовые решения, чтобы не изобретать сложные таймеры вручную.

Non‑blocking operations / Неблокирующие операции

Неблокирующие операции позволяют выполнять действия параллельно, не ожидая завершения других потоков. В Go это легко реализуется через горутины и каналы, в Java — через CompletableFuture или неблокирующие структуры данных.

// 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");
        }
    }
}
  
Совет: неблокирующие операции повышают отзывчивость системы. В Go это естественно через select с default, в Java используйте poll, CompletableFuture или асинхронные API.

Scheduler / Планирование (GMP модель)

Go использует модель GMP (Goroutine‑M‑Processor) для планирования: легкие горутины распределяются по системным потокам через P‑процессоры. В Java планировщик работает на уровне потоков ОС, а управление задачами лежит на ExecutorService.

// Go: простая демонстрация горутин и планировщика
package main

import (
    "fmt"
    "runtime"
)

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

func main() {
    runtime.GOMAXPROCS(2) // используем 2 системных потока
    for i := 0; i < 4; i++ {
        go worker(i)
    }

    // ждём, чтобы горутины завершились (для простоты)
    var input string
    fmt.Scanln(&input)
}
  
// Java: выполнение нескольких задач через ExecutorService
import java.util.concurrent.*;

public class SchedulerExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2); // 2 потока
        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);
    }
}
  
Совет: в Go горутины легкие и управляются планировщиком, что позволяет создавать тысячи задач без перегрузки. В Java используйте ExecutorService и пулы потоков для управления задачами, избегая создания слишком большого количества потоков.

Сводная таблица: Go vs Java по concurrency

Тема Go Java Комментарий
Rate‑Limiter Каналы + Ticker ScheduledExecutorService, Guava Go позволяет легко реализовать простой лимит, Java часто использует готовые библиотеки
Non-blocking select с default, горутины poll(), CompletableFuture, асинхронные API В Go natural non-blocking через каналы, в Java нужно использовать специфические структуры или API
Scheduler GMP (goroutine → P → M → OS thread) ExecutorService и пул потоков ОС Go может создавать тысячи легких горутин, Java лучше управлять количеством потоков через пул

Пример Worker Pool в Go и 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();
    }
}
  

Вывод / Итоги

Конкурентность в Go и Java реализована по разным философиям:

  • Go: легкие горутины, каналы, встроенный планировщик GMP, удобные неблокирующие конструкции.
  • Java: тяжелые потоки ОС, ExecutorService, CompletableFuture, готовые структуры для rate‑limiting и асинхронности.

Практические советы:

  • Для простых задач и тысяч операций используйте горутины в Go.
  • Для контроля concurrency в Java используйте пулы потоков и планировщики.
  • Неблокирующие операции помогают поддерживать отзывчивость, используйте select в Go и poll / CompletableFuture в Java.
  • Rate‑limiter нужен для API и внешних ресурсов: Go — простыми тикерами, Java — библиотеками.

Понимание этих различий позволяет быстро адаптировать опыт из одного языка в другой, используя сильные стороны каждой модели.

Предыдущие статьи серии: Базовые типы и структуры данных в Go vs Java | Generics, Reflection и продвинутые типы


🌐 in English
Всего лайков:0
Мой канал в социальных сетях
Отправляя email, вы принимаете условия политики конфиденциальности

Полезные статьи:

Go vs Java -  сравнение модели памяти: happens-before, visibility, reorder, synchronization events, write/read barriers
Модель памяти — это слой между программой и процессором. Современные CPU агрессивно оптимизируют выполнение: инструкции могут переставляться, данные могут храниться в кешах ядер, а операции могут выпо...
Современные архитектурные подходы: от монолита к событийным системам
Введение Архитектура — это не просто способ расположить классы и модули. Это язык, на котором система разговаривает со временем. Сегодня Java-разработчик живёт в мире, где границы между сервисами, по...
Типы данных в Java
Типы данных в Java Привет! С вами Виталий Лесных. В этом уроке курса «Основы Java для начинающих» разберем, что такое типы данных. Типы данных — это фундамент любого языка программирования. С их помо...

Новые статьи:

Resource cleanup, rate‑limiting strategies, bounded vs unbounded channels - в Go vs Java | Паттерны, идиомы и лучшие практики Go
Продолжаем серию статей для разработчиков, которые хотят изучить Go на основе знаний Java, и наоборот. В этой статье мы обсудим три ключевые темы: Resource Cleanup (освобождение ресурсов), Rate-Limiti...
Разбираем: Rate‑limiter, non‑blocking operations, scheduler  Go vs Java | Concurrency часть 4
Эта статья посвящена пониманию принципов работы с конкурентностью и синхронизацией в Go и Java. Мы рассмотрим ключевые подходы, такие как rate‑limiter, неблокирующие операции и планирование задач, сра...
Разбираем: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection в Go vs Java | Testing, Debugging и Profiling
Серия: Go для Java-разработчиков — разбор trace, профилирования и тестирования В этой статье мы разберем инструменты и практики для тестирования, отладки и профилирования в Go. Для Java-разработчика ...
Fullscreen image