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 и синхронизацию между потоками/горутинами.


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

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

Java под микроскопом: стек, куча и GC на примере кода
Схема - Java Memory Model - Heap / Non-Heap / Stack Heap (память для объектов) Создаёт объекты через new. Young Generation: Eden + Survivor. Old Generation: объекты, пережившие несколько сборок G...
Переменные и Константы в Java
Переменные в Java — понятие, типы, область видимости и константы Всем привет! С вами Виталий Лесных. В этом уроке разберём, что такое переменные в Java, зачем они нужны, какие бывают типы, как объявля...
Эволюция языка Java v1–v25: ключевые фичи
Легенда ✅ — Production (можно использовать в продакшне) ⚠️ — Preview / Incubator (экспериментальная, не для продакшна, в скобках указана версия, когда стало Production) Таблица версий Версия...

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

Многопоточность в Go и Java: типы задач и паттерны решения
Многопоточность — это не просто «запустить миллион потоков и пусть считают». Это искусство эффективно использовать ресурсы процессора и памяти, безопасно обрабатывать данные и правильно распределять з...
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 in...
Go vs Java -  сравнение модели памяти: happens-before, visibility, reorder, synchronization events, write/read barriers
Модель памяти — это слой между программой и процессором. Современные CPU агрессивно оптимизируют выполнение: инструкции могут переставляться, данные могут храниться в кешах ядер, а операции могут выпо...
Fullscreen image