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:

Go vs Java - comparison of memory models: happens-before, visibility, reorder, synchronization events, write/read barriers
Memory model is a layer between the program and the processor. Modern CPUs aggressively optimize execution: instructions may be reordered, data may be stored in core caches, and operations may be perf...
Map internals from random order to bucket evacuation | Go ↔ Java
In this article, we will examine the internal structure of maps / hash tables in Go and Java. If you are a Java developer used to HashMap, you will be interested in how differently Go thinks. If you a...
Multithreading in Go and Java: types of tasks and solution patterns
Multithreading is not just about "starting a million threads and letting them calculate". It is the art of efficiently using CPU and memory resources, safely processing data, and properly distributing...

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