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.
Useful Articles:
New Articles: