Go vs. Java - Comparing Memory Models - Part 2: Atomic Operations, Preemption, Defer/Finally, Context, Escape Analysis, GC, False Sharing

Atomic operations

Atomic operations ensure correct execution of variable operations without race conditions, guaranteeing a happens-before between reads and writes.

Go example:

import "sync/atomic"

var counter int32

func increment() {
atomic.AddInt32(&counter, 1) // atomic increment
}

Java example:

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger();

void increment() {
counter.incrementAndGet(); // atomic increment
}

Preemption / Scheduler Effects

Preemption is when the scheduler interrupts the execution of a thread/goroutine. This is important for happens-before, since the order of execution is not guaranteed without synchronization.

Go example:

go func() {
println("Task 1")
}()
go func() {
println("Task 2")
}()
runtime.Gosched() // gives other goroutines a chance to execute

Java example:

Thread t1 = new Thread(() -> System.out.println("Task 1"));
Thread t2 = new Thread(() -> System.out.println("Task 2"));
t1.start();
t2.start();
Thread.yield(); // gives other threads a chance

Defer / Finally as synchronous endpoints

Defer in Go and finally in Java are executed after the function/block exits, making them convenient for releasing resources and synchronizing.

Go example:

mu.Lock()
defer mu.Unlock() // guarantees unlocking even during a panic

Java example:

lock.lock();
try {
// code
} finally {
lock.unlock(); // guaranteed release
}

Context propagation / cancellation (Go)

Context allows you to manage the lifecycle of goroutines and the propagation of cancellation events, which is directly related to happens-before.

Go example:

ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Cancelled")
}
}(ctx)

cancel() // cancels execution of a goroutine

Escape analysis / locals -> heap

Escape analysis determines whether a variable goes to the heap or remains on the stack. Indirectly affects visibility, since objects on the heap are accessible to other goroutines/threads.

Go example:

func getPointer() *int {
x := 42
return &x // x "escapes" into the heap
}

Java example:

class Box { int value; }

Box makeBox() {
Box b = new Box();
return b; // object on the heap, accessible to other threads
}

GC pause / stop-the-world

Pausing the garbage collector can temporarily stop all threads/goroutines. In Go and Java, this is important for understanding ordering and happens-before.

Go example:

runtime.GC() // Forced GC
//→ Initiates GC, trying to minimize pauses, but a complete stop-world pause is possible.

Java example:

System.gc(); // Forced GC
//→ JVM recommendation to perform GC, not guaranteed.

False sharing / cache effects

False sharing occurs when multiple threads modify different variables located in the same cache line. This can impair visibility and performance.

Go example:

type PaddedCounter struct {
value int64
_ [7]int64 // padding to prevent false sharing
}
Go: _[7]int64 — padding prevents false sharing on 64-bit systems, but the cache line size can be larger (usually 64 bytes). Sometimes, more precise packages/structures are used for alignment.

Java example:

class PaddedCounter {
volatile long value;
long p1,p2,p3,p4,p5,p6,p7; // padding
}
Java: volatile isn't really needed for padding; its main purpose is visibility between threads. The key here is padding, so that the value appears as a separate cache line. On modern JVMs, you can use @Contended annotations (with the -XX:-RestrictContended JVM option enabled) to have the JVM add padding automatically.
Concept Go example Java example What influences
Atomic operations atomic.AddInt32(&counter, 1) counter.incrementAndGet() Guarantees atomicity, happens-before between reads and writes
Preemption / Scheduler runtime.Gosched() Thread.yield() Affects the execution order of goroutines/threads and the visibility of changes
Defer / Finally defer mu.Unlock() finally { lock.unlock(); } Resource release, synchronous points after function/block exit
Context propagation / cancellation context.WithCancel() Happens-before propagation via goroutine cancellation
Escape analysis / local → heap return &x return new Box() Determines where the object is stored (stack/heap), indirectly affects visibility
GC pause / stop-the-world runtime.GC() System.gc() Temporarily stops threads, affects ordering and synchronization
False sharing / cache effects type PaddedCounter struct { value int64; _ [7]int64 } class PaddedCounter { volatile long value; long p1,p2,p3,p4,p5,p6,p7; } May disrupt visibility and reduce performance

Summary

In this article, we covered advanced aspects of the memory model and multithreading in Go and Java. Atomic operations, preemption, defer/finally, context propagation, escape analysis, GC pause, and false sharing—all of these mechanisms directly or indirectly affect happens-before, change visibility, and code execution order.

Go and Java have similar concepts, but they are implemented differently: Go often uses lightweight goroutines and context for lifecycle management, while Java uses heavyweight threads and volatile/atomic classes. Understanding these nuances is critical for writing correct, safe, and high-performance code in a multithreaded environment.

The final table demonstrates how specific constructs and mechanisms affect the memory model and synchronization between threads/goroutines.


🌐 На русском
Total Likes:0

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

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

Useful Articles:

How to keep a legacy project from dying and give it another 10 years
Signs of a legacy project: how to recognize an old ship A legacy is not just old code. It is a living organism that has survived dozens of changes, team shifts, outdated technologies, and numerous tem...
Evolution of Java language v1–v25: key features
Legend ✅ — Production (can be used in production) ⚠️ — Preview / Incubator (experimental, not for production, version when it became Production is indicated in parentheses) Version Table Ver...
Arithmetic operators
In this lesson, we will talk about arithmetic operations and operators. In programming, operators are commands that perform specific actions: mathematical, string, logical, or comparison operations. T...

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