Аккаунт Войти / Регистрация
Тема
Размер текста
Synchronization and security in Go vs Java | Concurrency part 2

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.

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

Мой канал в социальных сетях
Отправляя email, вы принимаете условия политики конфиденциальности

Полезные статьи:

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…
Low-level mechanisms - part 2 | Go ↔ Java
In this article, we gathered the key low-level mechanisms of Go that most often raise questions for developers coming from Java. We will consider: unsafe.Pointer, struct alignment, pointer arithmetic,…
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…

Новые статьи:

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. …