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
```
🌐 На русском
Total Likes:0

Оставить комментарий

My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Основы параллельности в Go для Java-разработчиков | Сoncurrency часть 1
Если вы Java-разработчик, привыкший к потокам и ExecutorService, Go предлагает более лёгкий и удобный подход к параллельной обработке — goroutine и каналы. В этой статье мы разберём ключевые концепции...
Variables and Constants in Java
Variables in Java — concept, types, scope, and constants Hello everyone! This is Vitaly Lesnykh. In this lesson, we will discuss what variables are in Java, why they are needed, what types there are, ...
Atomic vs Mutex, Blocking vs Non-Blocking, Read/Write Splitting (RWMutex), Logging | Concurrency Patterns and Best Practices part 5 | Go ↔ Java
In this article, we will analyze the key approaches to working with parallelism and synchronization in Go and Java. We will compare how the same tasks are solved in these languages, show idioms, patte...

New Articles:

Concurrency is not about “starting many threads”. It’s about agreements between them. Imagine a restaurant kitchen: — cooks (threads / goroutines) — orders (tasks) — and the main question: how do th...
When HashMap starts killing production: the engineering story of ConcurrentHashMap
Imagine a typical production service. 32 CPU hundreds of threads configuration / session / rate limits cache tens of thousands of operations per second And somewhere inside — a regular Map. At first...
Zero Allocation in Java: what it is and why it matters
Zero Allocation — is an approach to writing code in which no unnecessary objects are created in heap memory during runtime. The main idea: fewer objects → less GC → higher stability and performance. ...
Fullscreen image