Back to All Concepts
ConcurrencyPatternsLow LevelGoJavaAdvanced

Concurrency Patterns

Essential patterns for managing concurrent execution. Mutex, Semaphores, Monitors, and modern approaches like the Actor Model and CSP.

Last updated: By the ScaleWiki Editorial Team

Why Concurrency is Hard

Concurrency introduces non-determinism. Without proper synchronization, you get race conditions, deadlocks, and starvation.

This guide covers the fundamental patterns used to serialize access to shared resources.

1. Mutex (Mutual Exclusion)

The simplest synchronization primitive. Only one thread can hold the lock at a time.

Use Case: Protecting a shared counter or critical section.

go
package main

import (
	"sync"
)

type SafeCounter struct {
	mu sync.Mutex
	v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()   // Lock before accessing map
	defer c.mu.Unlock() // Ensure unlock happens even if panic
	c.v[key]++
}
Click to expand code...

[!WARNING] Forget to unlock? Deadlock. Using defer in Go or try-finally in Java/Python is critical.

2. Semaphore

A mutex that allows NN concurrent accesses.

Use Case: Limiting connections to a database or rate limiting.

3. Read-Write Lock (RWMutex)

Allows multiple readers OR one writer.

Use Case: Caches where reads vastly outnumber writes.

  • RLock(): Blocks if a writer holds lock. Allows other readers.
  • Lock(): Blocks until all readers finish. Exclusive access.

4. Monitor Pattern

Combines a Mutex with Condition Variables (Wait/Notify).

Pattern:

  1. Acquire Lock.
  2. Check Condition. If false, Wait (release lock, sleep).
  3. Do work.
  4. Notify waiting threads.
  5. Release Lock.

Java Example (Synchronized):

java
public synchronized void produce(Item item) {
    while (queue.isFull()) {
        wait(); // Releases lock and sleeps
    }
    queue.add(item);
    notifyAll(); // Wakes up consumers
}

5. Communicating Sequential Processes (CSP)

Popularized by Go. "Do not communicate by sharing memory; share memory by communicating."

Key Idea: Use Channels to pass data between Goroutines. No explicit locks.

go
package main

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)
}

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2
    }
}
Click to expand code...

6. Actor Model

Popularized by Erlang and Akka (Scala/Java).

Key Concepts:

  • Actor: Fundamental unit of computation.
  • Mailbox: Actors communicate only by sending messages.
  • State: Private, never shared. Modified only by processing messages.
  • No Locks: Messages are processed sequentially.

7. The Failure Modes: Know Your Enemies

Every pattern above exists to prevent one of four classic bugs. Being able to name the bug you're defending against is half of concurrency competence:

  • Race condition: two threads read-modify-write the same data with interleaved steps. counter++ is three operations (load, add, store); two threads doing it "simultaneously" can lose an increment. Defense: mutex, atomics, or eliminating sharing.
  • Deadlock: thread A holds lock 1 and wants lock 2; thread B holds lock 2 and wants lock 1. Both wait forever. The four Coffman conditions must all hold — break any one. The standard practical fix: global lock ordering (every thread acquires locks in the same canonical order, e.g., by memory address or by ID).
  • Livelock: threads keep acting (retrying, backing off, retrying...) but make no progress — two people stepping side-to-side in a hallway forever. Defense: randomized backoff so retries desynchronize.
  • Starvation: some thread never gets the resource because others always win — e.g., a write lock that never gets granted because readers keep arriving. Defense: fair queuing (FIFO locks) or writer-preference policies in RW locks.

The Dining Philosophers, in One Paragraph

Five philosophers, five forks, each needs two forks to eat. If everyone grabs their left fork simultaneously, everyone waits for a right fork forever — deadlock by symmetry. The classic solutions map to real techniques: number the forks and always acquire the lower number first (lock ordering); allow at most four philosophers at the table (a semaphore capping concurrency); or make picking up both forks atomic (a coarser critical section). Every real-world multi-lock design is dining philosophers wearing a costume.

8. Optimistic Concurrency: Don't Lock, Detect

Locks are pessimistic — they assume conflict and pay for it up front. The optimistic alternative assumes conflict is rare and detects it after the fact:

  • Compare-and-swap (CAS): the hardware primitive under every lock-free structure. "Set X to new only if it still equals expected; tell me if I lost." On failure, re-read and retry. Java's AtomicLong, Go's atomic package, and every spinlock are built on this.
  • Optimistic locking in databases: add a version column; UPDATE ... WHERE id = 42 AND version = 7. Zero rows updated means someone got there first — reload and retry. This is how systems avoid holding row locks across user think-time.
  • MVCC: databases like PostgreSQL let readers see a consistent snapshot while writers create new row versions — readers never block writers and vice versa. The same philosophy as RW locks, achieved without blocking.

The trade: under low contention, optimistic approaches are dramatically faster (no lock overhead). Under high contention they degrade badly — everyone retries constantly, wasting work. Measure your conflict rate before choosing.

9. Practical Guidance: Choosing in Real Systems

  1. Reach for the highest-level tool that fits. A worker pool consuming a channel/queue solves 80% of application concurrency without a single explicit lock. Drop to mutexes for shared caches and counters; drop to atomics only when a profiler tells you the mutex is hot.
  2. Shrink critical sections ruthlessly. Hold locks for nanoseconds, not milliseconds: never do I/O, RPC calls, or allocation-heavy work while holding a mutex. The pattern "copy under lock, process outside lock" resolves most contention problems.
  3. Prefer immutability at boundaries. Data passed between threads as immutable snapshots (or ownership-transferred messages, as CSP and actors enforce) cannot race by construction. This is why the Actor and CSP columns in the table say "No shared state" — the pattern makes the bug inexpressible.
  4. These patterns scale up. A distributed lock (via Redis or ZooKeeper) is a mutex with network failure modes; leader election is a distributed monitor; a message queue is a durable channel; rate limiting is a semaphore over time. Single-machine concurrency is the training ground for distributed systems.

Comparison

PatternShared State?Locking?Best For
MutexYesExplicitFine-grained state protection
RW LockYesExplicitRead-heavy shared caches
CSP (Channels)NoImplicitStream processing, Pipelines
Actor ModelNoNoneDistributed systems, Fault tolerance
Atomics / CASYesHardwareCounters, Flags (High Perf)
Optimistic (versioning)YesNone (detect & retry)Low-conflict updates, DB rows

Related Concepts

About ScaleWiki

ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.

Read more about our Editorial Guidelines & Authorship.

Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.

Related Articles