Друкарня від WE.UA

Java Memory Model ↔ PostgreSQL MVCC. Спільний фундамент конкурентності?

Зміст

Два світи — одна проблема

Java Memory Model та PostgreSQL MVCC/Isolation Levels — це дві відповіді на одне й те саме фундаментальне питання: за яких умов зміна, зроблена одним учасником (потоком / транзакцією), стає видимою для іншого?

На перший погляд — це абсолютно різні доменні області. JMM оперує CPU-кешами, compiler reordering, happens-before ребрами. PostgreSQL — xmin/xmax, snapshot'ами, WAL, MVCC-версіями рядків. Але якщо подивитись на рівень абстракції вище, виявляється, що обидві системи вирішують ідентичну задачу з ідентичним набором trade-off'ів: consistency vs performance під конкурентним доступом до shared mutable state.

Цей пост — спроба побудувати систематичну карту відповідностей між цими двома світами. Не поверхневі аналогії, а глибокі структурні паралелі, які допоможуть розробнику, що добре знає одну сторону, інтуїтивно зрозуміти іншу.


Спільний фундамент: Shared Mutable State

У Java конкурентність стає проблемою лише тоді, коли є спільний змінюваний стан — три умови, прибери будь-яку — і race condition зникає:

Умова

Java

PostgreSQL

Shared

Heap-об'єкт доступний з кількох потоків

Рядок у таблиці доступний з кількох транзакцій

Mutable

Поле можна змінити після створення об'єкта

Рядок можна UPDATE/DELETE

Concurrent access

2+ потоки, хоча б один пише

2+ транзакції, хоча б одна пише

І рішення теж симетричні:

Стратегія усунення

Java

PostgreSQL

Прибрати Shared

Thread confinement, ScopedValue

Partitioning per connection, advisory locks per tenant

Прибрати Mutable

final fields, Records, immutable collections

Append-only таблиці, INSERT замість UPDATE, event sourcing

Контролювати Access

synchronized, Lock, Atomic*, volatile

Row locks, table locks, SERIALIZABLE isolation, SELECT FOR UPDATE

Це не просто гарна аналогія — це один і той самий фундаментальний принцип інженерії конкурентних систем.


Visibility: happens-before ↔ Isolation Levels

Головне питання обох моделей

В Java: «Коли запис, зроблений потоком A, гарантовано видимий для читання потоком B?»

В PostgreSQL: «Коли зміна, зроблена транзакцією T1 (навіть уже committed), стає видимою для SELECT в транзакції T2?»

Відповідь в обох випадках — не автоматично. Потрібен явний механізм, який встановлює гарантію видимості.

JMM Happens-Before → PostgreSQL Isolation Guarantees

В Java без happens-before зв'язку JVM та CPU мають право тримати значення в кешах нескінченно (ви бачите stale data) або переставляти інструкції (ви бачите «неможливий» порядок подій). Це не баг — це оптимізація.

В PostgreSQL MVCC робить те саме: транзакція бачить snapshot бази, зроблений у певний момент часу. Навіть якщо інша транзакція вже зробила COMMIT — ваш snapshot може цього не відображати. Це теж не баг — це ізоляція.

Ось систематична відповідність:

READ UNCOMMITTED ↔ Відсутність happens-before (plain read у JMM)

PostgreSQL: Теоретично дозволяє dirty reads — бачити незакомічені дані інших транзакцій. (У PostgreSQL фактично не реалізований — READ UNCOMMITTED працює як READ COMMITTED.)

Java: Читання без volatile/synchronized — plain read. JMM не гарантує, що ви побачите останнє записане значення. Ви можете побачити stale значення з CPU-кеша, або навіть значення, яке «ще не мало б існувати» (через reordering).

Спільне: Мінімальні гарантії видимості. Максимальна продуктивність. Практично непридатно для коректної логіки.

READ COMMITTED ↔ volatile read / synchronized (per-statement visibility)

PostgreSQL: Кожен окремий SQL-оператор бачить все, що було committed на момент його початку. Але два послідовних SELECT в одній транзакції можуть бачити різні дані, якщо між ними хтось зробив COMMIT.

-- Транзакція T2, READ COMMITTED
SELECT balance FROM accounts WHERE id = 1; -- бачить 1000
-- T1 робить: UPDATE ... SET balance = 500; COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- бачить 500 (!)
-- Non-repeatable read

Java: volatile read гарантує visibility — ви побачите останнє значення, записане будь-яким потоком у цю volatile змінну. Але це гарантія per-access, а не per-block. Між двома volatile reads інший потік може змінити значення.

volatile int balance = 1000;

// Thread B:
int a = balance;     // бачить 1000 (volatile read — fresh)
// Thread A: balance = 500;
int b = balance;     // бачить 500 (!)
// Non-repeatable read — те саме!

Спільне: Гарантія «бачу щось committed / записане», але без консистентності між послідовними читаннями. Кожне окреме читання fresh, але сукупність читань може бути inconsistent.

Ключова різниця: У PostgreSQL «committed» означає durability (WAL fsync). У Java «видимо» означає store buffer flushed / cache coherence. Але pattern — ідентичний.

REPEATABLE READ ↔ snapshot-based reads / synchronized block

PostgreSQL: Транзакція отримує snapshot на початку першого SQL-оператора. Всі подальші SELECT бачать однакову картину світу, навіть якщо інші транзакції commit-или зміни. Ви працюєте з «замороженим» станом бази.

-- Транзакція T2, REPEATABLE READ
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- бачить 1000
-- T1: UPDATE ... SET balance = 500; COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- все ще 1000!
-- Snapshot isolation — картина світу фіксована
COMMIT;

Java: synchronized блок створює happens-before ребро: при вході в блок ви «бачите» всі зміни, зроблені до виходу з того самого монітора. Всередині блоку ваша view є consistent — жоден інший потік з тим самим монітором не може вносити зміни. Це аналог «snapshot» — на час виконання блоку ваш стан фіксований.

synchronized (account) {
    int a = account.balance;  // бачить 1000
    // ніхто з тим самим монітором не може змінити
    int b = account.balance;  // гарантовано 1000
    // Repeatable read в межах synchronized
}

Спільне: Фіксована, узгоджена картина стану на момент «входу». Зміни інших учасників не видимі до завершення scope.

SERIALIZABLE ↔ повна синхронізація (global lock / SSI)

PostgreSQL: SERIALIZABLE використовує Serializable Snapshot Isolation (SSI) — відстежує залежності між транзакціями (read-write, write-read, write-write) і abort-ить транзакцію, якщо виявлено цикл. Результат еквівалентний тому, ніби транзакції виконувались послідовно.

-- T1 (SERIALIZABLE): SELECT sum(balance) FROM accounts; UPDATE accounts SET balance = balance + 100 WHERE id = 1;
-- T2 (SERIALIZABLE): SELECT sum(balance) FROM accounts; UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- PostgreSQL виявить rw-залежність → abort одну з транзакцій
-- ERROR: could not serialize access due to read/write dependencies

Java: Повна серіалізація через один глобальний lock або synchronized на одному об'єкті для всіх операцій. Або — якщо використовувати optimistic підхід — CAS-loop з retry при конфлікті (що семантично ідентично Postgres SSI з retry).

// Pessimistic (як одна глобальна черга в PostgreSQL)
synchronized (globalLock) {
    int sum = account1.balance + account2.balance;
    account1.balance += 100;
}

// Optimistic (як SSI з retry при конфлікті)
while (true) {
    long stamp = stampedLock.tryOptimisticRead();
    int sum = account1.balance + account2.balance;
    if (stampedLock.validate(stamp)) { // нічого не змінилось?
        break; // успіх
    }
    // конфлікт → retry (як SERIALIZABLE abort + retry)
}

Спільне: Гарантія, що результат еквівалентний послідовному виконанню. Максимальна consistency, мінімальна concurrency.


Повна таблиця відповідностей: Visibility

Рівень ізоляції (PG)

JMM / Java аналог

Що бачите

Trade-off

READ UNCOMMITTED

plain read (no HB)

Можливо stale / dirty

Макс. швидкість, мін. гарантії

READ COMMITTED

volatile read

Останнє committed / recorded

Per-read freshness, не repeatable

REPEATABLE READ

synchronized block entry

Snapshot на момент входу

Consistent view, можливі write конфлікти

SERIALIZABLE

Global lock або CAS+retry

Повна серіалізація

Макс. consistency, мін. throughput


Locks: synchronized ↔ PostgreSQL Locking

Гранулярність блокувань

І в Java, і в PostgreSQL ключове архітектурне рішення — на якому рівні блокувати. Занадто грубий lock — вбиває throughput. Занадто тонкий — overhead або deadlock.

Гранулярність

Java

PostgreSQL

Глобальний

synchronized (GlobalLock.class)

LOCK TABLE ... IN ACCESS EXCLUSIVE MODE

Об'єкт / Таблиця

synchronized (this)

LOCK TABLE ... IN ROW EXCLUSIVE MODE

Поле / Рядок

ReentrantLock на конкретне поле

SELECT ... FOR UPDATE (row-level lock)

Бакет / Партиція

ConcurrentHashMap (lock per bucket)

Partitioned table + partition-level locks

ConcurrentHashMap ↔ PostgreSQL Row-Level Locks

ConcurrentHashMap в Java 8+ — яскрава аналогія з PostgreSQL row-level locking:

CHM: Lock-гранулярність — один бакет. CAS для вставки в порожній бакет (без блокування). synchronized на head node для зайнятого. Два потоки, що пишуть у різні бакети, НЕ блокують один одного.

PostgreSQL MVCC: Кожен рядок має xmin/xmax. UPDATE створює нову версію рядка. Два запити, що оновлюють різні рядки, НЕ блокують один одного. Lock утримується тільки на конкретному рядку (через tuple-level lock у shared memory).

Властивість

ConcurrentHashMap

PostgreSQL Row Locks

Гранулярність

Per bucket (head node)

Per row (ctid + lock table)

Неконфліктні операції

Різні бакети → паралельно

Різні рядки → паралельно

Конфліктна операція

synchronized на head node

Blocking на row lock (xt_lock wait)

Read під час write

Volatile read, weakly consistent

MVCC — бачимо стару версію

SELECT FOR UPDATE ↔ synchronized + read

SELECT ... FOR UPDATE — це семантичний еквівалент Java-патерну «захопи lock, прочитай, зміни, відпусти»:

-- PostgreSQL
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- row locked
-- інші транзакції чекають
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- lock released
// Java
synchronized (account) {               // lock acquired
    int balance = account.getBalance(); // read
    account.setBalance(balance - 100);  // modify
}                                       // lock released

В обох випадках: захопити ексклюзивний доступ → прочитати → змінити → відпустити. Паттерн ідентичний.

Advisory Locks ↔ ReentrantLock

PostgreSQL advisory locks — це еквівалент ReentrantLock у Java: explicit, application-level, не прив'язані до конкретного рядка чи таблиці.

-- PostgreSQL: advisory lock по ID ресурсу
SELECT pg_advisory_lock(42);  -- lock
-- critical section
SELECT pg_advisory_unlock(42);  -- unlock

-- З timeout (аналог tryLock)
SELECT pg_try_advisory_lock(42);
// Java: ReentrantLock
lock.lock();          // lock
try {
    // critical section
} finally {
    lock.unlock();    // unlock
}

// З timeout
lock.tryLock(5, TimeUnit.SECONDS);

Властивість

ReentrantLock

pg_advisory_lock

Explicit acquire/release

lock() / unlock()

pg_advisory_lock() / pg_advisory_unlock()

Try with timeout

tryLock(timeout)

pg_try_advisory_lock()

Reentrant

Так

Так (session-level)

Scope

JVM process

Database cluster

Видимість у моніторингу

Thread dump, JFR

pg_locks view


Optimistic vs Pessimistic: CAS ↔ MVCC conflict detection

Це, можливо, найглибша і найважливіша паралель.

Pessimistic Concurrency Control

Java: synchronized / ReentrantLockспершу блокуємо, потім працюємо. Якщо ресурс зайнятий — чекаємо.

PostgreSQL: SELECT FOR UPDATEспершу блокуємо рядок, потім читаємо/оновлюємо. Якщо рядок locked — чекаємо.

Обидва підходи гарантують відсутність конфліктів, але платять за це простоєм (threads waiting / transactions waiting).

Optimistic Concurrency Control

Java: CAS-операції (AtomicReference, StampedLock.tryOptimisticRead). Працюємо без блокування, а в кінці перевіряємо: «нічого не змінилось, поки я працював?» Якщо змінилось — retry.

// StampedLock optimistic read
long stamp = lock.tryOptimisticRead();  // no lock!
double x = this.x, y = this.y;         // read without blocking
if (!lock.validate(stamp)) {            // check: was there a write?
    // conflict → fallback to pessimistic
    stamp = lock.readLock();
    try { x = this.x; y = this.y; } finally { lock.unlockRead(stamp); }
}

PostgreSQL: MVCC + REPEATABLE READ / SERIALIZABLE. Працюємо з snapshot без блокування. При COMMIT PostgreSQL перевіряє: «чи не конфліктує мій запис з тим, що зробили інші?» Якщо конфліктує — abort (serialization failure).

-- REPEATABLE READ: optimistic approach
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;  -- snapshot: 1000
-- Інша транзакція: UPDATE balance = 500 WHERE id = 1; COMMIT;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ERROR: could not serialize access due to concurrent update
-- → retry transaction

Паттерн ідентичний:

1. Прочитати стан без блокування (snapshot / optimistic read)
2. Виконати обчислення
3. Спробувати «зафіксувати» результат (CAS / COMMIT)
4. Якщо конфлікт — retry з нуля

Фаза

Java CAS / Optimistic

PostgreSQL MVCC

Read without lock

head.get() / tryOptimisticRead()

SELECT (snapshot read)

Compute

Обчислення нового значення

Обчислення нового рядка

Validate + commit

compareAndSet() / validate()

COMMIT (conflict check)

Conflict → retry

CAS loop

Serialization failure → retry

Коли що обирати (правило однакове для обох світів)

Optimistic (CAS / MVCC + SERIALIZABLE + retry): коли конфліктів мало. Більшість операцій пройдуть без конкуренції. Retry — рідкість.

Pessimistic (Lock / SELECT FOR UPDATE): коли конфліктів багато. Краще зачекати, ніж зробити роботу даремно і retry'їти. Або коли retry дорогий (side effects, зовнішні виклики).


Deadlock: однакова проблема, однакове рішення

Java Deadlock

// Thread A:
synchronized (account1) {
    synchronized (account2) { transfer(); }
}
// Thread B:
synchronized (account2) {
    synchronized (account1) { transfer(); }
}
// → Deadlock: A тримає account1, чекає account2; B тримає account2, чекає account1

PostgreSQL Deadlock

-- T1:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- lock row 1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- wait for row 2...

-- T2:
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;   -- lock row 2
UPDATE accounts SET balance = balance + 50 WHERE id = 1;   -- wait for row 1...
-- → Deadlock: T1 тримає row 1, чекає row 2; T2 тримає row 2, чекає row 1

Рішення — ідентичне в обох світах

Завжди захоплюйте локи в однаковому порядку.

// Java: ORDER BY id
int first = Math.min(a.id, b.id), second = Math.max(a.id, b.id);
synchronized (accounts[first]) {
    synchronized (accounts[second]) { transfer(); }
}
-- PostgreSQL: ORDER BY id
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- Обидві транзакції завжди блокують спершу id=1, потім id=2
UPDATE ...;
COMMIT;

Виявлення: Java JVM автоматично детектить monitor deadlocks (jstack показує). PostgreSQL має deadlock detector (дефолт: deadlock_timeout = 1s), який abort-ить одну з транзакцій.


Reordering і Phantom Reads: коли світ змінюється під ногами

Java Reordering

Без happens-before JVM може переставити інструкції. Класичний приклад — double-checked locking без volatile:

// Thread A:
instance = new Singleton();
// Компілятор може: 1) allocate memory, 2) assign reference, 3) run constructor
// Thread B бачить non-null reference на неініціалізований об'єкт!

Об'єкт «існує» (reference not null), але його стан ще не ініціалізований. Ви бачите «частковий» стан, якого ніколи не мало б бути.

PostgreSQL Phantom Reads

-- T1 (READ COMMITTED):
SELECT count(*) FROM orders WHERE status = 'pending';  -- 5
-- T2: INSERT INTO orders (status) VALUES ('pending'); COMMIT;
SELECT count(*) FROM orders WHERE status = 'pending';  -- 6!
-- «Фантомний» рядок з'явився між двома однаковими запитами

Спільна суть

В обох випадках: стан світу змінився між двома спостереженнями, і ви бачите inconsistency. Рішення теж симетричне:

Java

PostgreSQL

Проблема

Reordering → частковий / inconsistent стан

Phantom read → inconsistent набір рядків

Рішення

volatile / synchronized (establish HB)

REPEATABLE READ / SERIALIZABLE (snapshot)

Ціна

Memory fences → менша оптимізація

Snapshot maintenance → memory + abort ризик


MVCC Versioning ↔ CopyOnWrite

PostgreSQL MVCC і Java CopyOnWriteArrayList мають вражаюче спільну архітектуру:

PostgreSQL MVCC: UPDATE не змінює існуючий рядок. Натомість створюється нова версія рядка з новими xmin/xmax. Старі транзакції продовжують бачити стару версію (через свій snapshot). Нові — бачать нову. VACUUM потім прибирає мертві версії.

CopyOnWriteArrayList: Write не змінює існуючий масив. Створюється новий масив з модифікацією. Потоки, що ітерують по старому масиву, продовжують бачити стару версію. Нові читачі бачать нову. GC прибирає старий масив коли останній reader закінчить.

Аспект

PostgreSQL MVCC

CopyOnWriteArrayList

Write strategy

Створити нову версію рядка

Створити новий масив

Old readers

Бачать стару версію (snapshot)

Ітерують по старому масиву

New readers

Бачать нову версію

Отримують reference на новий масив

Cleanup

VACUUM (видалити dead tuples)

GC (зібрати unreachable масив)

Ідеально для

Read-heavy workload, рідкий write

Read-heavy, рідкий write (listeners, configs)

Ціна write

Дублювання рядка + WAL

Копіювання всього масиву

Це один і той самий патерн: immutable snapshots для readers + copy-on-write для writers + deferred cleanup.


Transaction Scope ↔ StructuredTaskScope

Ще одна глибока паралель: PostgreSQL транзакція та Java StructuredTaskScope мають ідентичну семантику lifecycle management.

PostgreSQL транзакція:

  • BEGIN → відкриває scope

  • Всі операції всередині — один атомарний блок

  • Якщо будь-яка операція впала — ROLLBACK всього

  • COMMIT → всі зміни видимі одночасно

  • Жодна зміна не «тече» за межі транзакції без COMMIT

StructuredTaskScope:

  • StructuredTaskScope.open() → відкриває scope

  • fork() створює дочірні задачі всередині scope

  • ShutdownOnFailure: перша помилка → cancel решти (ROLLBACK аналог)

  • scope.join() + close() → завершення scope

  • Жодна задача не «тече» за межі scope

// Java Structured Concurrency
try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
    var user = scope.fork(() -> fetchUser(id));
    var orders = scope.fork(() -> fetchOrders(id));
    scope.join();           // wait for all (як COMMIT чекає завершення)
    return combine(user.get(), orders.get());
}
// scope.close() = всі задачі guaranteed завершені (як END TRANSACTION)
-- PostgreSQL Transaction
BEGIN;
    SELECT * FROM users WHERE id = $1;
    SELECT * FROM orders WHERE user_id = $1;
COMMIT;  -- всі зміни атомарно видимі (або ROLLBACK при помилці)

Властивість

PostgreSQL Transaction

StructuredTaskScope

Atomic scope

BEGIN...COMMIT

try-with-resources

All-or-nothing

ROLLBACK при помилці

ShutdownOnFailure → cancel all

First-wins

Не типово (але можна через PL/pgSQL)

ShutdownOnSuccess → cancel rest

Bounded lifetime

Транзакція не «тече»

Задача не «тече» за scope

Нested scopes

SAVEPOINT

Nested StructuredTaskScope

Observability

pg_stat_activity

Thread dump з parent-child деревом


ScopedValue ↔ Transaction-local state

Java ThreadLocal → PostgreSQL session variables:

// Java: ThreadLocal — глобальний, mutable, leak-prone
ThreadLocal<UserContext> ctx = new ThreadLocal<>();
ctx.set(user);  // хто завгодно може перезаписати
// Забули remove() → memory leak у thread pool
-- PostgreSQL: session variable — живе до кінця сесії
SET myapp.current_user = 'alice';
-- Може бути перезаписана будь-коли
-- «Тече» між транзакціями в одній сесії

Java ScopedValue → PostgreSQL transaction-scoped config:

// Java: ScopedValue — immutable, bounded, auto-cleanup
ScopedValue.where(CURRENT_USER, user).run(() -> {
    // значення доступне тут і в усіх child scopes
    // автоматично зникає при виході
});
-- PostgreSQL: transaction-local SET
BEGIN;
SET LOCAL myapp.current_user = 'alice';
-- Доступна тільки в цій транзакції
-- Автоматично зникає при COMMIT/ROLLBACK
COMMIT;
-- myapp.current_user більше не 'alice'

SET LOCAL — це PostgreSQL-еквівалент ScopedValue: bounded lifetime (transaction scope), автоматичне очищення, не «тече» за межі scope.


Фінальна мапа відповідностей

Концепція Java

Концепція PostgreSQL

Спільна суть

Happens-before

Isolation level guarantees

За яких умов зміна видима

volatile read

READ COMMITTED (per-statement)

Бачу committed, але non-repeatable

synchronized block

REPEATABLE READ (snapshot)

Consistent view протягом scope

Global lock / CAS+retry

SERIALIZABLE (SSI)

Еквівалент послідовного виконання

synchronized / Lock

SELECT FOR UPDATE / row lock

Pessimistic: блокую, потім працюю

CAS / StampedLock optimistic

MVCC + serialization failure + retry

Optimistic: працюю, потім валідую

CopyOnWriteArrayList

MVCC versioning

Immutable snapshots + copy-on-write

ConcurrentHashMap (per-bucket)

Row-level locks

Fine-grained locking

ReentrantLock

pg_advisory_lock

Explicit application-level lock

Deadlock detection (JVM)

Deadlock detection (PG)

Lock ordering + auto-detection

StructuredTaskScope

Transaction (BEGIN...COMMIT)

Bounded lifetime, all-or-nothing

ScopedValue

SET LOCAL

Scoped state, auto-cleanup

Reordering → inconsistency

Phantom reads

Світ змінюється між спостереженнями

ThreadLocal leak

Session variable leak

Unbounded lifetime → resource leak

VarHandle acquire-release

Visibility rules між snapshot'ами

Fine-grained control над видимістю

Thread dump / JFR

pg_stat_activity / pg_locks

Діагностика конкурентних проблем


Практичний висновок: один mental model для двох світів

Коли ви проектуєте конкурентний Java-код — ви приймаєте ті самі рішення, що й при виборі рівня ізоляції в PostgreSQL:

  1. Яку гарантію видимості я потребую? → volatile (READ COMMITTED) vs synchronized (REPEATABLE READ) vs global lock (SERIALIZABLE)

  2. Pessimistic чи optimistic? → Lock і чекати (SELECT FOR UPDATE) vs CAS і retry при конфлікті (SERIALIZABLE + retry)

  3. Яка гранулярність блокування? → Global lock (TABLE LOCK) vs per-object (row lock) vs lock-free (MVCC)

  4. Як забезпечити bounded lifetime? → StructuredTaskScope (Transaction) + ScopedValue (SET LOCAL)

  5. Як діагностувати? → Thread dump + JFR (pg_stat_activity + pg_locks + pg_stat_statements)

Розуміння одного світу автоматично поглиблює розуміння іншого, тому що під капотом — це одна й та сама інженерна дисципліна: керування видимістю та доступом до shared mutable state під конкурентним навантаженням.

Статті про вітчизняний бізнес та цікавих людей:

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Oleksandr Klymenko
Oleksandr Klymenko@overpathz we.ua/overpathz

Java Software Engineer

40Довгочити
7.8KПрочитання
98Підписники
На Друкарні з 19 квітня

Більше від автора

Це також може зацікавити:

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Це також може зацікавити: