Go vs Java - memory model comparison: happens-before, visibility, reorder, synchronization events, write/read barriers

The memory model is a layer between the program and the processor. Modern CPUs aggressively optimize execution: instructions can be reordered, data can be stored in core caches, and operations can be performed speculatively. Without strict rules, two threads could see completely different values ​​of the same variable.

Both Go and Java introduce a formal memory model, which defines when changes from one thread become visible to another.

The Main Idea - Happens-before

Property Go Java
Memory Model Go Memory Model Java Memory Model
The Main Rule happens-before happens-before
Basic Synchronization channels, mutex, atomic volatile, synchronized, Lock
Guarantee visibility through synchronization events through synchronization events
Philosophy shared memory by communicating shared memory + synchronization

The main rule of memory models is the happens-before relationship. If operation A happens-before B, then all memory changes made in A are guaranteed to be visible in B.


A happens-before B

The Problem Without Synchronization

Go


package main

import "fmt"

var x int
var ready bool

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

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

Intuitively, if ready == true, then x is already equal to 42. But the processor can reorder instructions or store data in the CPU cache.

Java


int x = 0;
boolean ready = false;

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

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

In Java, the same problem arises: a thread might see ready=true, but the old value of x.

Solution: Synchronization

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)
}

Closing the channel creates a happens-before connection between threads.

Java (volatile)


volatile boolean ready;
int x;

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

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

Volatile writes guarantee happens-before between threads.

Instruction Reordering

The compiler and CPU may reorder instructions for optimization, if this does not affect the result within a single thread.


x = 1
y = 2

can be executed as


y = 2
x = 1

Visibility

The visibility issue arises because of processor caches. Each core has its own memory cache, and changes to one core may not be immediately visible to another.


CPU1: x = 10
CPU2: reads x

CPU2 may see the old value.

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

Philosophy of languages

Go


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

The core idea of ​​Go is to pass data through channels between goroutines.

Java


Thread
↓
shared memory
↓
synchronization

Java has historically used a shared memory model with synchronization.

Data Race

A data race occurs when:

  • two threads access the same variable
  • at least one thread writes
  • no synchronization

Go has a built-in detector:


go run -race main.go

Preliminary Summary

Characteristic Go Java
Basic Model happens-before happens-before
Synchronization channels + mutex volatile + locks
Philosophy message passing shared memory
Simplicity usually simpler often more complex

An interesting paradox of multithreading: most developers think that concurrency is about threads. In fact, it's about memory. Threads are just actors, and the real battle occurs in the processor caches.

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

A read barrier is a check when reading a pointer or reference, used by some garbage collectors to correctly handle objects that may have been moved around in memory during a concurrent GC.

Example in Go:


child := node.child
// The runtime can perform a read barrier to ensure the reference is valid.

Example in Java (ZGC or Shenandoah):


Node child = node.child;
// The JVM may insert a read barrier when reading a movable object

Final Comparison: Go vs. Java Memory Model

Go and Java use a similar concept of happens-before, which ensures changes are visible between threads with proper synchronization. The main difference is in philosophy: Go advocates sharing data through channels (shared memory by communicating), while Java traditionally uses shared memory with synchronization (shared memory + synchronization).

Aspect Go Java Comments
Basic Concept happens-before happens-before Both models guarantee visibility of changes with proper synchronization
Philosophy Share memory by communicating Shared memory + synchronization Go prefers channels, Java prefers locks and volatile
Synchronization channels, mutex, WaitGroup, atomic volatile, synchronized, Lock, CountDownLatch Different mechanisms, but create happens-before relationships
Visibility Guarantees Via channels and atomic operations Via volatile and synchronized Ensures visibility between threads
Instruction Reordering CPU/compiler can reorder, channels/atomic operations fix happens-before CPU/compiler can reorder, volatile/synchronized operations insert memory barriers Reordering without synchronization is possible in both languages
Write Barrier Inserted automatically by the runtime when writing references for the GC The JVM inserts a write barrier in modern GCs (G1, ZGC) Ensures correct operation of the garbage collector
Read Barrier Used by the runtime when reading references to track relocated objects Used in ZGC/Shenandoah for safe reading of objects Helps the GC correctly read current data
Data Race Detection go run -race JVM analysis tools and static tools Data race detection Memory
Ease of use for developers Higher — channels simplify synchronization Medium — requires explicit handling of locks and volatility Go makes concurrency safer by default
Summary The memory model is safe, the philosophy is message passing The memory model is safe if the rules are followed, the philosophy is shared memory The main difference is in the approach to synchronization and data transfer

The main practical lesson: most concurrency bugs arise not from the number of threads, but from incorrect synchronization and misunderstandings of the order of instruction execution. Using channels in Go and synchronized/volatile in Java helps manage memory safely.

For Go developers: Do not communicate by sharing memory; share memory by communicating.
For Java developers: Proper use of volatile, synchronized, and concurrent structures ensures predictability.

Using built-in tools like go run -race in Go and data race analysis in Java helps identify bugs early.


🌐 На русском
Total Likes:0
My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4
This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We ll cover key approaches such as rate-limiter, non-blocking operations, and task scheduli...
Современный подход к параллелизму в Java - Fork/Join Framework, CompletableFuture и виртуальные потоки (Project Loom)
```html id="b6v9kc" ``` Virtual threads are especially useful for high-volume I/O operations—for example, when processing HTTP requests or accessing external APIs. The code remains linear and readabl...
Understanding Multithreading in Java Through Collections and Atomics
1️⃣ HashMap / TreeMap / TreeSet (not thread-safe) HashMap: Structure: array of buckets + linked lists / trees (for collisions). Under the hood: put/remove modifies the bucket array and possibly reord...

New Articles:

Generics, Reflection and Channels - Go vs Java | Types - Language
In this article we will analyze advanced type system features in Go: generics (type parameters), reflection, and channel types for concurrency. We will compare Go and Java approaches, so Java develope...
Let's look at: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection in Go vs Java | Testing, Debugging and Profiling
Series: Go for Java Developers — analysis of trace, profiling and testing In this article we will analyze tools and practices for testing, debugging and profiling in Go. For a Java developer this wil...
Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4
This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We ll cover key approaches such as rate-limiter, non-blocking operations, and task scheduli...
Fullscreen image