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