Go vs Java - сравнение модели памяти: happens-before, visibility, reorder, synchronization events, write/read barriers

Модель памяти — это слой между программой и процессором. Современные CPU агрессивно оптимизируют выполнение: инструкции могут переставляться, данные могут храниться в кешах ядер, а операции могут выполняться спекулятивно. Без строгих правил два потока могли бы видеть совершенно разные значения одной и той же переменной.

И Go, и Java вводят формальную memory model, которая определяет когда изменения одного потока становятся видимыми для другого.

Основная идея - Happens-before

Свойство Go Java
Модель памяти Go Memory Model Java Memory Model
Основное правило happens-before happens-before
Основная синхронизация channels, mutex, atomic volatile, synchronized, Lock
Гарантия видимости через synchronization events через synchronization events
Философия share memory by communicating shared memory + synchronization

Главное правило моделей памяти — отношение happens-before. Если операция A happens-before B, то все изменения памяти, сделанные в A, гарантированно видны в B.


A happens-before B

Проблема без синхронизации

Go


package main

import "fmt"

var x int
var ready bool

func writer() {
    x = 42
    ready = true
}

func reader() {
    if ready {
        fmt.Println(x)
    }
}

Интуитивно кажется, что если ready == true, то x уже равен 42. Но процессор может изменить порядок инструкций или держать данные в кеше CPU.

Java


int x = 0;
boolean ready = false;

void writer() {
    x = 42;
    ready = true;
}

void reader() {
    if (ready) {
        System.out.println(x);
    }
}

В Java возникает та же проблема: поток может увидеть ready=true, но старое значение x.

Решение: синхронизация

Go (channel)


package main

import "fmt"

var x int
var ch = make(chan struct{})

func writer() {
    x = 42
    close(ch)
}

func reader() {
    <-ch
    fmt.Println(x)
}

Закрытие канала создаёт happens-before связь между потоками.

Java (volatile)


volatile boolean ready;
int x;

void writer() {
    x = 42;
    ready = true;
}

void reader() {
    if (ready) {
        System.out.println(x);
    }
}

Запись volatile гарантирует happens-before между потоками.

Instruction Reordering

Компилятор и CPU могут менять порядок инструкций ради оптимизации, если это не влияет на результат внутри одного потока.


x = 1
y = 2

может выполняться как


y = 2
x = 1

Visibility (видимость)

Проблема видимости возникает из-за кешей процессора. Каждое ядро имеет собственный кеш памяти, и изменения одного ядра могут быть не сразу видны другому.


CPU1: x = 10
CPU2: читает x

CPU2 может увидеть старое значение.

Synchronization events

Go

  • channel send → receive
  • mutex unlock → lock
  • WaitGroup.Done → Wait
  • atomic operations
  • close(channel)

Java

  • synchronized exit → enter
  • volatile write → read
  • Lock.unlock → lock
  • Thread.start
  • Thread.join
  • Future.get

Философия языков

Go


Do not communicate by sharing memory;
share memory by communicating.

Основная идея Go — передавать данные через каналы между goroutines.

Java


Thread
   ↓
shared memory
   ↓
synchronization

Java исторически использует модель общей памяти с синхронизацией.

Data Race

Data race возникает когда:

  • два потока обращаются к одной переменной
  • хотя бы один поток пишет
  • нет синхронизации

В Go есть встроенный детектор:


go run -race main.go

Предварительный итог

Характеристика Go Java
Основная модель happens-before happens-before
Синхронизация channels + mutex volatile + locks
Философия message passing shared memory
Простота обычно проще часто сложнее

Интересный парадокс многопоточности: большинство разработчиков думают, что concurrency — это про потоки. На самом деле это про память. Потоки — лишь актёры, а настоящая борьба происходит в кешах процессора.

Write Barrier

Write barrier — это специальная инструкция рантайма, вставляемая при записи указателей или ссылок на объекты в память. Она нужна для работы сборщика мусора (GC), чтобы корректно отслеживать новые и изменённые объекты во время параллельной сборки.

Концептуальный пример в Go:


node.child = newNode
// runtime автоматически добавляет write barrier для GC

В Java JVM использует аналогичные write barriers в современных сборщиках (G1, ZGC, Shenandoah) для отслеживания изменений ссылок:


node.child = newNode;
// JVM вставляет write barrier для безопасной работы GC

Read Barrier

Read barrier — это проверка при чтении указателя или ссылки, используемая некоторыми сборщиками мусора, чтобы корректно обработать объекты, которые могли быть перемещены в памяти во время concurrent GC.

Пример в Go:


child := node.child
// runtime может выполнить read barrier, чтобы убедиться, что ссылка актуальна

Пример в Java (ZGC или Shenandoah):


Node child = node.child;
// JVM может вставить read barrier при чтении перемещаемого объекта

Итоговое сравнение: Go vs Java Memory Model

Go и Java используют схожую концепцию happens-before, которая гарантирует видимость изменений между потоками при правильной синхронизации. Основное отличие в философии: Go пропагандирует передачу данных через каналы (share memory by communicating), а Java традиционно использует разделяемую память с синхронизацией (shared memory + synchronization).

Аспект Go Java Комментарии
Основная концепция happens-before happens-before Обе модели гарантируют видимость изменений при правильной синхронизации
Философия Share memory by communicating Shared memory + synchronization Go предпочитает каналы, Java – locks и volatile
Синхронизация channels, mutex, WaitGroup, atomic volatile, synchronized, Lock, CountDownLatch Разные механизмы, но создают happens-before связи
Visibility Guarantees Через каналы и atomic операции Через volatile и synchronized Обеспечивает видимость между потоками
Instruction Reordering CPU/компилятор могут менять порядок, каналы/atomic фиксируют happens-before CPU/компилятор могут менять порядок, volatile/synchronized вставляют memory barriers В обоих языках возможны reorder без синхронизации
Write Barrier Вставляется автоматически runtime при записи ссылок для GC JVM вставляет write barrier в современных GC (G1, ZGC) Обеспечивает корректность работы сборщика мусора
Read Barrier Используется runtime при чтении ссылок для отслеживания перемещённых объектов Используется в ZGC/Shenandoah для безопасного чтения объектов Помогает GC корректно читать актуальные данные
Data Race Detection go run -race JVM инструментами анализа и static tools Обнаружение гонок на памяти
Простота для разработчика Выше — каналы упрощают синхронизацию Средняя — требует явной работы с locks и volatile Go делает concurrency безопаснее по умолчанию
Итог Модель памяти безопасная, философия — message passing Модель памяти безопасная при соблюдении правил, философия — shared memory Главная разница в подходе к синхронизации и передачи данных

Главный практический вывод: большинство багов многопоточности возникают не из-за количества потоков, а из-за неверной синхронизации и неправильного представления о порядке выполнения инструкций. Использование каналов в Go и synchronized/volatile в Java помогает безопасно управлять памятью.

Для Go разработчиков: Do not communicate by sharing memory; share memory by communicating.
Для Java разработчиков: правильное использование volatile, synchronized и concurrent-структур обеспечивает предсказуемость.

Использование встроенных инструментов вроде go run -race в Go и анализ data races в Java помогает выявлять ошибки на раннем этапе.


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

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

От микросервисной революции к эпохе эффективности
Период 2010–2020 годов можно назвать эпохой разделения и масштабирования. Системы стали слишком большими, чтобы оставаться монолитами. Решением стали микросервисы — маленькие автономные приложения, ра...
Эволюция языка 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