# Concurrency Programming (2): Language Memory Models — Rules Programmers Can Rely On

## Table of Contents

- [0\. Continue from the Previous Article](#0-continue-from-the-previous-article)
- [1\. Why Does the Language Need a Memory Model Layer?](#1-why-does-the-language-need-a-memory-model-layer)
- [2\. How Should Programmers Understand Memory Model Rules?](#2-how-should-programmers-understand-memory-model-rules)
  - [2.1 Java Memory Model](#21-java-memory-model)
  - [2.2 Go Memory Model](#22-go-memory-model)
  - [2.3 CPython Concurrency Semantics](#23-cpython-concurrency-semantics)
    - [2.3.1 GIL Mode](#231-gil-mode)
    - [2.3.2 Free-threaded Mode](#232-free-threaded-mode)
    - [2.3.3 So What Should Python Programs Rely On?](#233-so-what-should-python-programs-rely-on)
- [3\. Next: Mutexes](#3-next-mutexes)

---

## 0\. Continue from the Previous Article

The [previous article on runtime scheduling](https://thinkerqaq.com/en/articles/concurrency-series-01-5-thread-scheduling/) explained how execution units get CPU time. Here, we turn to the language rules governing concurrent memory accesses.

---

## 1\. Why Does the Language Need a Memory Model Layer?

Source code passes through a compiler and Runtime before it finally executes on a CPU. Compilers optimize code, and processors such as x86-64 and ARM64 do not allow exactly the same memory-access orderings.

If programmers had to study every compiler, Runtime, and CPU separately, portable concurrent programming would be difficult to sustain.

We therefore also need a language memory model layer. It defines the rules programmers need to understand, while the underlying Compiler / Runtime implements those rules differently for each platform:

```text
Java                                                Go                                                Python
 │                                                   │                                                   │
 ▼                                                   ▼                                                   ▼
synchronized / volatile / AtomicInteger             sync.Mutex / sync/atomic / channel                 threading.Lock / queue.Queue
 │                                                   │                                                   │
 ▼                                                   ▼                                                   ▼
Java Memory Model                                   Go Memory Model                                     CPython Concurrency Semantics
 │                                                   │                                                   │
 └───────────────────────────────────────────────────┼───────────────────────────────────────────────────┘
                                                     │
                                       Compiler / Runtime implementation
                                       abstracts away hardware differences
                                                     │
                                            ┌────────┴────────┐
                                            │                 │
                                            ▼                 ▼
                                         x86-64             ARM64
```

---

## 2\. How Should Programmers Understand Memory Model Rules?

There are three dimensions to look at:

- **Atomicity**
- **Visibility**
- **Ordering**

In theory, each concurrency primitive has corresponding memory-model rules that describe which of these properties it provides.

We will not go deeper here. Instead, we will reuse the same examples from earlier:

```text
count++
```

and:

```text
counter = 1
ready = true
```

We will use the language memory model to answer three questions:

| Question | Specifically |
| --- | --- |
| Atomicity | Why is `count++` not atomic? |
| Visibility | After A writes `counter = 1`, why is B not guaranteed to observe `counter = 1`? |
| Ordering | When B observes `ready = true`, why is B not guaranteed to observe the earlier `counter = 1`? |

---

### 2.1 Java Memory Model

**Atomicity**

Suppose a process contains this variable:

```text
count = 0
```

Now there are two execution units:

```text
Thread A                    Thread B

count++                     count++
```

If A and B each execute once, is the final result guaranteed to be:

```text
count = 2
```

No.

Logically, `count++` can be broken down into three steps:

```text
read count
compute count + 1
write count back
```

So the following interleaving is possible:

```text
initial: count = 0

Thread A                    Thread B

read count -> 0
compute count + 1 -> 1
                            read count -> 0
                            compute count + 1 -> 1
write count -> 1
                            write count -> 1
```

The final value becomes:

```text
count = 1
```

Both threads complete one `+1`, but because they read the same old value `0`, both compute `1`, and one of the updates is overwritten.

Therefore, ordinary `count++` cannot be treated as atomic.

---

**Visibility**

Now consider:

```java
int counter = 0;

// Thread A
counter = 1;

// Thread B
System.out.println(counter);
```

That is:

```text
Thread A                    Thread B

counter = 1                 read counter
```

In Java, Visibility is commonly expressed in terms of happens-before: whether a write is guaranteed to be observed by another thread depends on whether a happens-before relationship exists between the write and the read.

JLS §17.4.5 gives a very direct description of happens-before:

> “If one action happens-before another, then the first is visible to and ordered before the second.”

In other words, if we can establish:

```text
Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── happens-before ───────┘
```

then Thread A's write is guaranteed to be visible to Thread B's read.

But in the current code:

```text
Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └── no happens-before ──────┘
```

no such cross-thread happens-before relationship has been established.

Therefore, Thread B is not guaranteed to observe `1`.

---

**Ordering**

Return to the second example:

```java
// Thread A
counter = 1;
ready = true;

// Thread B
if (ready) {
    System.out.println(counter);
}
```

That is:

```text
Thread A                    Thread B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter
```

In Java, Ordering can also be expressed in terms of happens-before. If one action happens-before another, then the first action—and memory writes that precede it in the same thread—are ordered before the second action. Through the transitivity of happens-before, those earlier writes are also visible to reads that follow the second action.

Applied to this example: within Thread A, `counter = 1` comes before `ready = true`; within Thread B, reading `ready` comes before reading `counter`. But the two threads are still missing the cross-thread happens-before relationship from `ready = true` to `read ready == true`.

So the current code still does not establish a complete happens-before chain between Thread A and Thread B:

```text
Thread A                    Thread B

counter = 1
ready = true                read ready == true
     │                            │
     │                            ▼
     │                       read counter
     │
     └──── no cross-thread happens-before
```

Therefore, even if B observes:

```text
ready == true
```

B is still not guaranteed to observe:

```text
counter == 1
```

---

### 2.2 Go Memory Model

**Atomicity**

In Go, we can approach Atomicity through the Go Memory Model's definition of a data race: it first identifies which concurrent accesses constitute a data race, then excludes atomic accesses provided by `sync/atomic` from that definition.

The Go Memory Model states:

> “Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.”

It then explains that synchronization mechanisms such as channels, `sync`, and `sync/atomic` can serialize these accesses. Its definition of a data race explicitly excludes atomic accesses provided by `sync/atomic`.

But:

```go
counter++
```

is only an ordinary increment operation, not an atomic access provided by `sync/atomic`.

Therefore, if two goroutines concurrently execute:

```go
counter++
```

they perform concurrent reads and writes on the same `counter`, which constitutes a data race.

So ordinary `counter++` cannot be treated as an atomic concurrent update.

---

**Visibility**

Continue with:

```go
var counter int

// Goroutine A
counter = 1

// Goroutine B
fmt.Println(counter)
```

Split into two columns:

```text
Goroutine A                 Goroutine B

counter = 1                 read counter
```

In Go, Visibility can also be expressed in terms of happens-before. For an ordinary read `r` to observe a write `w`, the Go Memory Model requires `w` to be visible to `r`; the first condition is:

> “w happens before r.”

Applied to our example, if we can establish:

```text
Goroutine A                 Goroutine B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── happens-before ───────┘
```

then the later read can reliably observe that write.

But in the current code:

```text
Goroutine A                 Goroutine B

counter = 1                 read counter
     │                           ▲
     │                           │
     └── no happens-before ──────┘
```

no such relationship has been established, and the program contains a data race.

Therefore, the second goroutine cannot rely on always observing:

```text
counter == 1
```

---

**Ordering**

Now consider:

```go
// Goroutine A
counter = 1
ready = true

// Goroutine B
if ready {
    fmt.Println(counter)
}
```

Split into two columns:

```text
Goroutine A                 Goroutine B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter
```

In Go, Ordering can likewise be expressed in terms of happens-before. This is essentially the same pattern used by the Go Memory Model under **Incorrect synchronization**: one goroutine writes data and then a flag; another goroutine observes the flag and then reads the earlier data.

The Go Memory Model states:

> “Even if this occurs, it does not imply that reads happening after r will observe writes that happened before w.”

Applied to our example:

```text
Goroutine A                 Goroutine B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter

        no happens-before guarantee
```

So:

```text
observing ready == true
```

does not imply:

```text
counter must be observed as 1
```

This is very similar to the Java example.

Both Java and Go use happens-before to describe important Visibility and Ordering guarantees.

---

### 2.3 CPython Concurrency Semantics

Python differs from Java and Go in one important respect:

Python does not define one unified concurrency memory model for all interpreter implementations at the same level as the JMM or Go Memory Model.

So this article focuses on the most widely used implementation: **CPython**.

We will continue to use the same two examples:

```python
counter += 1
```

and:

```python
counter = 1
ready = True
```

and examine Atomicity, Visibility, and Ordering.

#### 2.3.1 GIL Mode

Before returning to `counter`, first clarify one question:

> **What does the GIL actually protect?**

The GIL is first and foremost a mechanism used by the CPython Runtime to protect Python objects and interpreter-internal state.

The official Python C API documentation states the requirement directly:

> “only a thread that holds the GIL may operate on Python objects or invoke Python’s C API.”

In other words, in default GIL-enabled CPython, a thread must hold the GIL before it can operate on Python objects or call the Python C API.

The documentation then uses **Reference Count** to explain why such a lock is needed: if two threads increment the same object's reference count at the same time, the result may be incremented only once instead of twice.

Suppose a Python object currently has:

```text
refcount = 10
```

If two threads could modify that reference count concurrently:

```text
Thread A                    Thread B

read refcount = 10          read refcount = 10

refcount + 1                refcount + 1

write refcount = 11         write refcount = 11
```

Both threads increment the reference count once, yet the final result may be:

```text
expected: refcount = 12
actual:   refcount = 11
```

That would break CPython's management of Python object lifetimes.

So the GIL primarily protects:

```text
             GIL
              │
              ▼
      CPython Runtime
              │
      ┌───────┴────────┐
      ▼                ▼
Python Objects     Runtime State
      │
      ├── Reference Count
      └── Object Internals
```

There are two different layers here:

- **CPython Runtime layer**: how to safely access and maintain Python objects under concurrency
- **Application layer**: how to safely access and modify shared state under concurrency

The GIL primarily addresses the former. It does not make application code automatically thread-safe.

---

**Atomicity**

Now return to:

```python
counter += 1
```

The official Python FAQ directly addresses this kind of operation.

When discussing which operations are atomic in GIL-enabled CPython, it explicitly lists:

```python
i = i + 1
```

as a non-atomic operation:

> “These aren’t: `i = i+1`”

Therefore, even with the GIL, we cannot treat:

```python
counter += 1
```

as a single atomic concurrent update.

We need to distinguish:

```text
GIL
 │
 └── protection for CPython Runtime / Python Objects

counter += 1
 │
 └── a compound state update defined by the application
```

---

**Visibility**

Continue with the same example:

```python
counter = 0

# Thread A
counter = 1

# Thread B
print(counter)
```

Split into two columns:

```text
Thread A                    Thread B

counter = 1                 read counter
```

Here the distinction between CPython and Java / Go appears.

With Java and Go, we can continue to ask:

```text
write
 │
 │ happens-before ?
 ▼
read
```

because those languages define formal Memory Models.

Python, however, does not define a corresponding happens-before model that applies uniformly to all Python implementations.

So we cannot draw the CPython code above as:

```text
Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── Python happens-before ┘
```

and then derive a conclusion from some Python Language Memory Model rule, because there is no corresponding unified model of that kind.

Conversely, because no separate rule specifies such a guarantee, we should regard the outcome as uncertain. That uncertainty means it does not satisfy the Visibility requirement.

---

**Ordering**

Finally, consider:

```python
# Thread A
counter = 1
ready = True

# Thread B
if ready:
    print(counter)
```

Again, split into two columns:

```text
Thread A                    Thread B

counter = 1
ready = True                read ready == True
                                  │
                                  ▼
                             read counter
```

Likewise, there is no corresponding rule that specifies such a guarantee here, so we should regard the outcome as uncertain. That uncertainty means it does not satisfy the Ordering requirement.

---

#### 2.3.2 Free-threaded Mode

Starting with Python 3.13, CPython provides free-threaded builds in which the GIL can be disabled.

In GIL-enabled CPython:

```text
Thread A ──┐
           │
           ├── GIL ───> Python Code
           │
Thread B ──┘
```

In free-threaded CPython:

```text
Thread A ─────────────> CPU Core 0

Thread B ─────────────> CPU Core 1
```

Multiple threads can execute Python code in parallel.

But removing the GIL does not mean CPython no longer needs to protect its objects and Runtime state.

Python's official free-threading documentation explicitly states that built-in types such as `dict`, `list`, and `set` use internal locks to protect against concurrent modification:

> “Built-in types like `dict`, `list`, and `set` use internal locks”

These internal locks are an implementation mechanism of free-threaded CPython. They should not be interpreted as Python the language defining a unified Memory Model.

So the change is from:

```text
GIL-enabled

one global GIL
      │
      ▼
protect CPython Runtime
```

to:

```text
Free-threaded

finer-grained internal synchronization
      │
      ▼
protect CPython Runtime
```

For application code, the same three questions still exist.

Different paths, same destination. Removing the GIL does not mean CPython no longer needs synchronization; it moves from one global lock toward finer-grained locks, atomic operations, and Runtime coordination. In this respect, its implementation approach increasingly resembles Java and Go.

---

#### 2.3.3 So What Should Python Programs Rely On?

In summary, Java, Go, and CPython differ at this layer in an important way:

```text
Java
  │
  ▼
Java Memory Model
  │
  └── happens-before

Go
  │
  ▼
Go Memory Model
  │
  └── happens-before

Python
  │
  ▼
Concurrency semantics of different Interpreters
  │
  └── CPython
        ├── GIL-enabled
        └── Free-threaded
```

The summary is simple: Python does not have one unified Memory Model, at the same level as the JMM or Go Memory Model, that applies to every interpreter implementation. Therefore, when discussing concurrency safety in Python, first identify which Interpreter is being used, and rely on explicit synchronization tools and the concurrency guarantees they provide.

---

## 3\. Next: Mutexes

Earlier, we discussed the hardware layer; this article explains the rules at the language memory model layer.

The next article begins with the first concrete synchronization tool, Mutex, and follows it from the language memory model layer all the way down to the hardware layer.

---

> This article was first published on [ThinkerQAQ's personal blog](https://thinkerqaq.com/en/articles/concurrency-series-02-language-memory-model/?utm_source=hashnode&utm_medium=referral&utm_campaign=article_syndication) and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.
