Go vs Java - Сравниваем модели памяти - часть 2: atomic operations, preemption, defer/finally, context, escape analysis, GC, false sharing

Atomic operations

Atomic операции обеспечивают корректное выполнение операций с переменными без гонок, гарантируя happens-before между чтением и записью.

Go пример:

import "sync/atomic"

var counter int32

func increment() {
    atomic.AddInt32(&counter, 1)  // атомарное увеличение
}

Java пример:

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger();

void increment() {
    counter.incrementAndGet();  // атомарное увеличение
}

Preemption / Scheduler Effects

Preemption — это когда планировщик прерывает выполнение потока/горутины. Это важно для happens-before, так как порядок выполнения не гарантирован без синхронизации.

Go пример:

go func() {
    println("Task 1")
}()
go func() {
    println("Task 2")
}()
runtime.Gosched()  // даёт шанс другим горутинам выполниться

Java пример:

Thread t1 = new Thread(() -> System.out.println("Task 1"));
Thread t2 = new Thread(() -> System.out.println("Task 2"));
t1.start();
t2.start();
Thread.yield();  // даёт шанс другим потокам

Defer / Finally как синхронные точки

Defer в Go и finally в Java выполняются после выхода из функции/блока, что делает их удобным инструментом для освобождения ресурсов и синхронизации.

Go пример:

mu.Lock()
defer mu.Unlock()  // гарантирует разблокировку даже при panic

Java пример:

lock.lock();
try {
    // код
} finally {
    lock.unlock();  // гарантированное освобождение
}

Context propagation / cancellation (Go)

Context позволяет управлять жизненным циклом горутин и распространением событий отмены, что напрямую связано с happens-before.

Go пример:

ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
    select {
    case <-ctx.Done():
        fmt.Println("Cancelled")
    }
}(ctx)

cancel()  // отменяет выполнение горутины

Escape analysis / локальные -> heap

Escape analysis определяет, уйдёт ли переменная в heap или останется на стеке. Косвенно влияет на visibility, так как объекты на heap доступны другим горутинам/потокам.

Go пример:

func getPointer() *int {
    x := 42
    return &x  // x "escape" в heap
}

Java пример:

class Box { int value; }

Box makeBox() {
    Box b = new Box();
    return b;  // объект в heap, доступен другим потокам
}

GC pause / stop-the-world

Пауза сборщика мусора может временно остановить все потоки/горутины. В Go и Java это важно для понимания ordering и happens-before.

Go пример:

runtime.GC()  // принудительный вызов GC 
 //→  инициирует GC, стараясь минимизировать паузы, но полная стоп-ворлд пауза возможна.

Java пример:

System.gc();  // принудительный вызов GC  
//→ рекомендация JVM сделать GC, не гарантированно.

False sharing / cache effects

False sharing возникает, когда несколько потоков модифицируют разные переменные, находящиеся в одной cache line. Это может нарушать видимость и performance.

Go пример:

type PaddedCounter struct {
    value int64
    _ [7]int64  // padding для предотвращения false sharing
}
Go: _ [7]int64 — padding предотвращает false sharing на 64-битных системах, но размер cache line может быть больше (обычно 64 байта). Иногда используют более точные пакеты/структуры для alignment.

Java пример:

class PaddedCounter {
    volatile long value;
    long p1,p2,p3,p4,p5,p6,p7; // padding
}
Java: volatile на самом деле не нужен для padding, его основная задача — видимость между потоками. Здесь главное — padding, чтобы value оказался отдельной cache line. На современных JVM можно использовать аннотации @Contended (с включённой опцией JVM -XX:-RestrictContended) для того, чтобы JVM сам добавлял padding.
Концепция Go пример Java пример Что влияет
Atomic operations atomic.AddInt32(&counter, 1) counter.incrementAndGet() Гарантирует атомарность, happens-before между чтением и записью
Preemption / Scheduler runtime.Gosched() Thread.yield() Влияет на порядок выполнения горутин/потоков, видимость изменений
Defer / Finally defer mu.Unlock() finally { lock.unlock(); } Освобождение ресурсов, синхронные точки после выхода из функции/блока
Context propagation / cancellation context.WithCancel() Распространение happens-before через отмену горутин
Escape analysis / локальные → heap return &x return new Box() Определяет, где хранится объект (stack/heap), косвенно влияет на visibility
GC pause / stop-the-world runtime.GC() System.gc() Временная остановка потоков, влияет на ordering и синхронизацию
False sharing / cache effects type PaddedCounter struct { value int64; _ [7]int64 } class PaddedCounter { volatile long value; long p1,p2,p3,p4,p5,p6,p7; } Может нарушать видимость и снижать производительность

Итог

В этой статье мы подробно разобрали расширенные аспекты memory model и многопоточности в Go и Java. Atomic операции, preemption, defer/finally, context propagation, escape analysis, GC pause и false sharing — все эти механизмы напрямую или косвенно влияют на happens-before, видимость изменений и порядок выполнения кода.

Go и Java имеют похожие концепции, но реализуются они по-разному: Go часто использует lightweight горутины и context для управления жизненным циклом, а Java — тяжелые потоки и volatile/atomic классы. Понимание этих нюансов критично для написания корректного, безопасного и высокопроизводительного кода в многопоточной среде.

Итоговая таблица демонстрирует, как конкретные конструкции и механизмы влияют на memory model и синхронизацию между потоками/горутинами.


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

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

Context, propagation и cancellation patterns в Go vs Java | Паттерны, идиомы и лучшие практики Go
← Связанные статьи: Часть 1 — Error handling и defer в Go (Параллельность и синхронизация) | Паттерны, идиомы и лучшие практики Go 1. Context и его роль В Go context.Context используется для пере...
Арифметические операторы
В этом уроке речь пойдет про арифметические операции и операторы. В программировании операторы — это команды, выполняющие определённые действия: математические, строковые, логические или операции срав...
Основы параллельности в Go для Java-разработчиков | Сoncurrency часть 1
Если вы Java-разработчик, привыкший к потокам и ExecutorService, Go предлагает более лёгкий и удобный подход к параллельной обработке — goroutine и каналы. В этой статье мы разберём ключевые концепции...

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

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