Atomic vs Mutex, Blocking vs Non-Blocking, Read/Write Splitting (RWMutex), Logging | Concurrency Patterns and Best Practices part 5 | Go ↔ Java

In this article, we will analyze the key approaches to working with parallelism and synchronization in Go and Java. We will compare how the same tasks are solved in these languages, show idioms, patterns, and best practices. The material will be useful for both Java developers learning Go and Go developers who want to understand familiar Java approaches.

Atomic vs Mutex

Synchronization of access to data can be implemented using atomic operations or mutexes. Atomic operations are simpler and faster but are limited to simple types. Mutexes are more versatile but add overhead.

Go: Atomic

// Example of atomic increment
import (
    "fmt"
    "sync/atomic"
)

func main() {
    var counter int32 = 0
    atomic.AddInt32(&counter, 1) // atomic increment by 1
    fmt.Println(counter) // output: 1
}
// Comment: atomic.AddInt32 guarantees that the increment operation will be performed without races.

Java: Atomic

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);
        counter.incrementAndGet(); // atomic increment by 1
        System.out.println(counter.get()); // output: 1
    }
}
// Comment: AtomicInteger provides atomic operations without blocking.

Go: Mutex

import (
    "fmt"
    "sync"
)

func main() {
    var counter int
    var mu sync.Mutex

    mu.Lock()         // lock access
    counter++         // safe operation
    mu.Unlock()       // unlock
    fmt.Println(counter) // output: 1
}
// Comment: A mutex guarantees exclusive access to a resource.

Java: Mutex (synchronized)

public class MutexExample {
    private int counter = 0;

    public synchronized void increment() { // synchronized = mutex
        counter++;
    }

    public static void main(String[] args) {
        MutexExample ex = new MutexExample();
        ex.increment();
        System.out.println(ex.counter); // output: 1
    }
}
// Comment: synchronized locks the object while the method is executed.
Atomic operations are better used for simple counters and flags. Mutexes are needed for complex data structures or sequential operations.
In business, atomic is conveniently used for page visit counters, likes, transaction limits. Mutexes are useful for working with caches, task queues, or transactions where the integrity of a complex structure needs to be ensured.

Blocking vs Non‑Blocking

Blocking operations stop the thread until the task is completed. Non-blocking allow the execution of other tasks to continue. In Go, this is often channels and select, in Java — Future, CompletableFuture and NIO.

Go: Blocking Channel

ch := make(chan int)

go func() {
    ch <- 42 // blocks until someone reads it
}()

val := <-ch
fmt.Println(val) // output: 42
// Comment: the channel is synchronous by default, the write operation blocks the goroutine.

Java: BlockingQueue

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingExample {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1);

        new Thread(() -> {
            try {
                queue.put(42); // blocks until there is space
            } catch (InterruptedException e) { }
        }).start();

        Integer val = queue.take(); // blocks until an element appears
        System.out.println(val); // output: 42
    }
}
// Comment: BlockingQueue simplifies synchronization between threads.

Go: Non-Blocking

select {
case ch <- 42: // if can send
    fmt.Println("Sent")
default:
    fmt.Println("Channel busy, continuing")
}
// Comment: select with default allows not to block the goroutine.

Java: Non-Blocking (CompletableFuture)

import java.util.concurrent.CompletableFuture;

public class NonBlockingExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 42);

        future.thenAccept(val -> System.out.println("Result: " + val));

        System.out.println("Continuing execution");
// Comment: CompletableFuture does not block the thread, the result is processed asynchronously.
    }
}
Blocking is convenient for simple task queues and synchronization in tight spots. Non-blocking is critical for web services, stream processing, and highly loaded systems to avoid keeping threads waiting.

Read/Write Splitting (RWMutex)

If data is read more often than written, Read/Write locks are used. Go — sync.RWMutex, Java — ReentrantReadWriteLock.

Go: RWMutex

import "sync"

var mu sync.RWMutex
var data int

// Reading
mu.RLock()
fmt.Println(data)
mu.RUnlock()

// Writing
mu.Lock()
data = 100
mu.Unlock()
// Comment: RLock allows multiple goroutines to read simultaneously.

Java: ReentrantReadWriteLock

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class RWExample {
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private int data;

    public void read() {
        rw.readLock().lock();
        try {
            System.out.println(data);
        } finally {
            rw.readLock().unlock();
        }
    }

    public void write(int val) {
        rw.writeLock().lock();
        try {
            data = val;
        } finally {
            rw.writeLock().unlock();
        }
    }
}
// Comment: readLock allows multiple reads, writeLock allows exclusive modification.
RWLock is useful for caches, configurations, and shared resources where reading predominates over writing. Pros: high read concurrency. Cons: more complex management, potential deadlock with improper use.

Logging Best Practices

Logging helps to understand the behavior of the application. In Go, popular choices are log, zap, zerolog; in Java — SLF4J, Log4j, Logback. The main principle is — structured and asynchronous logs.

Go: Structured Logging

import (
    "go.uber.org/zap"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    logger.Info("User logged in", zap.String("user", "Alice"), zap.Int("id", 42))
}
// Comment: Structured logs make it easy to filter and analyze events.

Java: Structured Logging (SLF4J + Logback)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingExample {
    private static final Logger logger = LoggerFactory.getLogger(LoggingExample.class);

    public static void main(String[] args) {
        logger.info("User logged in: user={}, id={}", "Alice", 42);
    }
}
// Comment: SLF4J supports parameters, which reduces the overhead of formatting.
Use logging for auditing, monitoring, and debugging. Asynchronous and structured logs improve performance and readability.
Pattern Go Java Comment
Atomic sync/atomic AtomicInteger Fast, lock-free, suitable for simple numerical operations.
Mutex sync.Mutex synchronized or ReentrantLock Universal, for complex data structures, but more resource-intensive.
Blocking channels, BlockingQueue BlockingQueue, Future.get() Blocks the thread until the operation is complete.
Non-Blocking select with default CompletableFuture, NIO Does not block the thread, used in asynchronous and high-load systems.
RWLock sync.RWMutex ReentrantReadWriteLock Allows separating reads and writes, increasing parallelism.
Logging zap, log, zerolog SLF4J, Logback, Log4j Structured, asynchronous logs improve debugging efficiency.

Output / Result

Comparing Go and Java, it is clear that many concepts overlap, but the implementation and idioms differ. Go emphasizes lightweight goroutines and channels, atomic operations and RWMutex, while Java focuses on traditional threads, synchronized, ReentrantLocks, and CompletableFuture for asynchronous programming. For practice:

  • Use atomic operations for simple counters and flags.
  • Mutexes and RWLocks — for complex structures with frequent reading.
  • Blocking is suitable for task queues, Non-blocking — for web and high-load systems.
  • Log structured and asynchronously for monitoring and debugging.

Understanding these patterns and idioms will allow for easy transfer of skills between Go and Java, leveraging the advantages of each language and avoiding common pitfalls when working with concurrency and synchronization.


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

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

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

Useful Articles:

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,...
Understanding multithreading in Java through collections and atomics
Understanding Multithreading in Java through Collections and Atomics 1️⃣ HashMap / TreeMap / TreeSet (not thread-safe) HashMap: Structure: array of buckets + linked lists / trees (for collisions). U...
Internal structure of the Garbage Collector: Go ↔ Java
In this article, we will thoroughly examine the work of the garbage collector (Garbage Collector, GC) in Go and Java, discuss key internal mechanisms: concurrent mark &amp; sweep, mutator vs collector...

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