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:

Java under the Microscope: Stack, Heap, and GC using Code Examples
Diagram - Java Memory Model - Heap / Non-Heap / Stack Heap (memory for objects) Creates objects via new. Young Generation: Eden + Survivor. Old Generation: objects that have survived several GC c...
Modern approach to parallelism in Java - Fork/Join Framework, CompletableFuture, and virtual threads (Project Loom)
Preface The world of software has long ceased to be a calm ocean: today it is a turbulent ecosystem where every millisecond of application response can cost a company customers, reputation, or money. ...
Scheduler internals in Go ↔ Java: how your code is actually executed
When you write go func() or create a Thread in Java, it seems like you are managing concurrency. But in reality, you are passing the task to the scheduler. And this is where the real show begins. Go...

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