Go vs Java - comparison of memory models: happens-before, visibility, reorder, synchronization events, write/read barriers
Memory model is a layer between the program and the processor. Modern CPUs aggressively optimize execution: instructions may be reordered, data may be stored in core caches, and operations may 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 that defines when changes in one thread become visible to another.
Main idea - Happens-before
| Property | Go | Java |
|---|---|---|
| Memory model | Go Memory Model | Java Memory Model |
| Main rule | happens-before | happens-before |
| Main synchronization | channels, mutex, atomic | volatile, synchronized, Lock |
| Visibility guarantee | through synchronization events | through synchronization events |
| Philosophy | share memory by communicating | shared memory + synchronization |
The main rule of memory models is the relationship happens-before. If operation A happens-before B, then all memory changes made in A are guaranteed to be visible in B.
A happens-before B
Problem of Unsynchronized Access
Go
package main
import "fmt"
var x int
var ready bool
func writer() {
x = 42
ready = true
}
func reader() {
if ready {
fmt.Println(x)
}
}
It intuitively seems that if ready == true, then x is already 42. But the processor may change the order of instructions or hold 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);
}
}
The same problem occurs in Java: a thread may 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 relationship between threads.
Java (volatile)
volatile boolean ready;
int x;
void writer() {
x = 42;
ready = true;
}
void reader() {
if (ready) {
System.out.println(x);
}
}
Volatile write guarantees happens-before between threads.
Instruction Reordering
The compiler and CPU can change the order of instructions for optimization, if it does not affect the result within a single thread.
x = 1
y = 2
can be executed as
y = 2
x = 1
Visibility
The visibility problem arises from processor caches. Each core has its own memory cache, and changes from 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 main idea of Go is to pass data through channels between goroutines.
Java
Thread
↓
shared memory
↓
synchronization
Java historically uses a shared memory model with synchronization.
Data Race
A data race occurs when:
- two threads access the same variable
- at least one thread writes
- there is no synchronization
In Go, there is a built-in detector:
go run -race main.go
Preliminary result
| Characteristic | Go | Java |
|---|---|---|
| Main 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 reality, it is about memory. Threads are just actors, and the real struggle occurs in CPU caches.
Write Barrier
Write barrier — a special runtime instruction inserted when writing pointers or references to objects in memory. It is needed for the garbage collector (GC) to correctly track new and modified objects during parallel collection.
A conceptual example in Go:
node.child = newNode
// runtime automatically adds write barrier for GC
In Java, the JVM uses similar write barriers in modern collectors (G1, ZGC, Shenandoah) to track changes in references:
node.child = newNode;
// JVM inserts write barrier for safe GC operation
Read Barrier
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 in memory during concurrent GC.
Example in Go:
child := node.child
// runtime may perform a read barrier to ensure that the reference is valid
Example in Java (ZGC or Shenandoah):
Node child = node.child;
// JVM may insert a read barrier when reading a moved object
Final Comparison: Go vs Java Memory Model
Go and Java use a similar concept of happens-before, which guarantees visibility of changes between threads with proper synchronization. The main difference in philosophy: Go advocates data sharing through channels (share memory by communicating), while Java traditionally uses shared memory with synchronization (shared memory + synchronization).
| Aspect | Go | Java | Comments |
|---|---|---|---|
| Main 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 – locks and volatile |
| Synchronization | channels, mutex, WaitGroup, atomic | volatile, synchronized, Lock, CountDownLatch | Different mechanisms, but create happens-before relationships |
| Visibility Guarantees | Through channels and atomic operations | Through volatile and synchronized | Ensures visibility between threads |
| Instruction Reordering | CPU/compiler can change order, channels/atomic enforce happens-before | CPU/compiler can change order, volatile/synchronized insert memory barriers | Reordering is possible without synchronization in both languages |
| Write Barrier | Automatically inserted by runtime on write references for GC | JVM inserts write barrier in modern GC (G1, ZGC) | Ensures correctness of garbage collector operation |
| Read Barrier | Used by runtime when reading references to track moved objects | Used in ZGC/Shenandoah for safe object reading | Helps GC correctly read current data |
| Data Race Detection | go run -race |
JVM analysis tools and static tools | Detection of memory races |
| Developer Simplicity | Higher — channels simplify synchronization | Average — requires explicit handling of locks and volatile | Go makes concurrency safer by default |
| Conclusion | Memory model is safe, philosophy is message passing | Memory model is safe with rule adherence, philosophy is shared memory | The main difference is in the approach to synchronization and data transfer |
The main practical takeaway: most multithreading bugs arise not from the number of threads, but from incorrect synchronization and improper understanding of instruction execution order. Using channels in Go and synchronized/volatile in Java helps safely manage memory.
For Go developers: Do not communicate by sharing memory; share memory by communicating.
For Java developers: proper use of volatile, synchronized and concurrent structures provides predictability.
Using built-in tools like go run -race in Go and analyzing data races in Java helps to identify errors early.
```html ```
Оставить комментарий
Useful Articles:
New Articles: