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