Разбираем: 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, вы принимаете условия политики конфиденциальности

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

Context, propagation и cancellation patterns в Go vs Java | Паттерны, идиомы и лучшие практики Go
← Связанные статьи: Часть 1 — Error handling и defer в Go (Параллельность и синхронизация) | Паттерны, идиомы и лучшие практики Go 1. Context и его роль В Go context.Context используется для пере...
Stream vs For в Java: как писать максимально быстрый код
В Java производительность часто определяется не «красотой кода», а тем, как именно он взаимодействует с памятью, JIT-компилятором и CPU cache. Разберём, почему обычный for часто быстрее Stream, и как ...
Циклы в Java: for, while, do while, Операторы continue и break
Привет! С вами Виталий Лесных. Сегодня мы продолжим курс «Основы Java для начинающих» и разберём одну из важнейших тем программирования — циклы. Цикл — это повторение выполнения кода до тех пор, пок...

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

Конкурентность — это не про «запустить много потоков». Это про договорённости между ними. Представь кухню ресторана: — повара (потоки / горутины) — заказы (задачи) — и главный вопрос: как они коорди...
История начинается не с академической теории, а с типичной production-проблемы. Представьте сервис: 48 CPU 300+ потоков нагрузка 200k операций в секунду много shared state Команда использует обы...
Когда HashMap начинает убивать продакшн: инженерная история ConcurrentHashMap
Представьте обычный продакшн-сервис. 32 CPU сотни потоков кэш конфигурации / сессий / rate limits десятки тысяч операций в секунду И где-то внутри — обычный Map. Сначала всё выглядит безобидно. Map&...
Fullscreen image