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:

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 ...
Asynchrony and Reactivity in Java: CompletableFuture, Flow, and Virtual Threads
In modern Java development, there are three main approaches to asynchrony and concurrency: CompletableFuture — for single asynchronous tasks. Flow / Reactive Streams — for data flows with backpressur...
Современный подход к параллелизму в 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...

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