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.
Оставить комментарий
Useful Articles:
New Articles: