Synchronization and security in Go vs Java | Concurrency part 2

In the second part, we will dive into synchronization and safety of concurrent code in Go. For a Java developer, it is useful to see the analogs: synchronized, ReentrantLock, CountDownLatch, atomic operations, and threading schemes. We will consider the main tools of Go: Mutex, RWMutex, WaitGroup, atomic operations, race conditions, deadlocks, and fan-in / fan-out patterns.

1. Mutex / RWMutex

Mutex — a locking mechanism that allows protecting access to shared resources between goroutines. RWMutex allows separating locking into reading and writing: multiple readers can operate simultaneously, but writing blocks everyone.


// Go: Mutex
var m sync.Mutex
counter := 0

m.Lock()
counter++
m.Unlock()

// RWMutex
var rw sync.RWMutex
rw.RLock()   // reading
value := counter
rw.RUnlock()
rw.Lock()    // writing
counter = value + 1
rw.Unlock()

// Java: synchronized / ReentrantLock
ReentrantLock lock = new ReentrantLock();
int counter = 0;

lock.lock();
try {
    counter++;
} finally {
    lock.unlock();
}

// For reading/writing, ReentrantReadWriteLock is used
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock();
try {
    int value = counter;
} finally {
    rw.readLock().unlock();
}
rw.writeLock().lock();
try {
    counter = value + 1;
} finally {
    rw.writeLock().unlock();
}
Use Mutex when strict access control is needed. RWMutex is efficient for cases when reads are more frequent than writes.

2. WaitGroup

WaitGroup allows waiting for the completion of a group of goroutines. In Java, it is similar to CountDownLatch or Phaser.


// Go
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println("Task", n)
    }(i)
}
wg.Wait()

// Java
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    int n = i;
    new Thread(() -> {
        System.out.println("Task " + n);
        latch.countDown();
    }).start();
}
latch.await();
WaitGroup helps to safely wait for the completion of multiple tasks. It is important not to forget to call Done() for each added task.

3. Atomic operations

For simple synchronization of counters or flags, Go provides the sync/atomic package. In Java, similarly, AtomicInteger and other atomic classes.


// Go
var counter int32 = 0
atomic.AddInt32(&counter, 1)
value := atomic.LoadInt32(&counter)

// Java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
int value = counter.get();
Atomic operations are efficient for simple counters or flags. For complex structures, it is better to use Mutex.

4. Race conditions

A race condition occurs when multiple goroutines modify a shared resource simultaneously without synchronization. In Java, this is similar to multithreading errors without synchronized/lock.


// Go: race example
var counter int
for i := 0; i < 100; i++ {
    go func() { counter++ }()
}
fmt.Println(counter) // may be less than 100 due to race
Always analyze potential race conditions. Go has a tool go run -race for detecting races.

5. Deadlocks

Deadlock occurs when goroutines block each other forever, for example, due to cyclic dependencies on Mutex or channels.


// Go: simple deadlock
ch := make(chan int)
ch <- 1 // nobody is reading — block
Deadlocks are hard to catch. Plan the order of resource acquisition and use buffered channels where possible.

6. Fan-in / Fan-out

Fan-out — distributing tasks to several goroutines. Fan-in — collecting results into one channel. This is a pattern of efficient parallel data flow.


// Go: Fan-out / Fan-in
jobs := make(chan int, 5)
results := make(chan int, 5)

// Fan-out
for w := 1; w <= 3; w++ {
    go func(id int) {
        for j := range jobs {
            results <- j*2
        }
    }(w)
}

// Sending tasks
for j := 1; j <= 5; j++ {
    jobs <- j
}
close(jobs)

// Fan-in
for a := 1; a <= 5; a++ {
    fmt.Println(<-results)
}
Fan-in / Fan-out allows scaling task processing and collecting results safely. Use buffered channels to avoid blocking.

Conclusion

In this article, we discussed the key synchronization mechanisms in Go: Mutex / RWMutex for protecting resources, WaitGroup for waiting for task completion, atomic operations for simple counters, and also explored race conditions, deadlocks, and the fan-in / fan-out patterns.

For a Java developer, these constructs are familiar by functionality: synchronized/ReentrantLock, CountDownLatch, AtomicInteger, and others. It is important to understand when to use a mutex, when to use atomic operations, and how to safely collect results from multiple goroutines.

In the following articles, we will explore more complex multithreading patterns and performance optimizations so that you can build high-load and safe applications in Go, using Java experience.


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

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

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

Useful Articles:

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...
Modern architectural approaches: from monolith to event-driven systems
Introduction Architecture is more than just a way to arrange classes and modules. It is the language a system uses to communicate time. Today, Java developers live in a world where the boundaries bet...
Data types in Java
Data Types in Java Hello! This is Vitaly Lesnykh. In this lesson of the "Java Basics for Beginners" course, we will discuss what data types are. Data types are the foundation of any programming langu...

New Articles:

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. ...
Stream vs For in Java: how to write the fastest code possible
In Java, performance is often determined not by the "beauty of the code," but by how it interacts with memory, the JIT compiler, and CPU cache. Let s analyze why the usual for is often faster than Str...
Compiler, Build, and Tooling in Go and Java: how assembly, initialization, analysis, and diagnostics are organized in two ecosystems
This article is dedicated to a general overview of how the compiler, build, and tooling practices are arranged in Go, and how to better understand them through comparison with Java. We will not delve ...
Fullscreen image