SharedArrayBuffer and Atomics for Parallel JavaScript
Learn how to use SharedArrayBuffer and Atomics to share memory between threads, coordinate Web Workers, and build lock-free data structures in JavaScript.
What you'll learn
- ✓How SharedArrayBuffer enables shared memory between threads
- ✓Using Atomics for thread-safe reads, writes, and synchronization
- ✓Building practical multi-threaded patterns with Web Workers
Prerequisites
- •JavaScript Web Workers basics
- •Understanding of TypedArrays
Most JavaScript runs in a single thread. Web Workers let you spin up additional threads, but normally each worker gets its own isolated memory — data must be copied via postMessage. SharedArrayBuffer changes this by letting multiple threads read and write the same block of memory, and Atomics provides the low-level synchronization primitives to do so safely.
SharedArrayBuffer basics
A SharedArrayBuffer is like an ArrayBuffer, but it can be sent to a Web Worker without transferring ownership. Both the main thread and the worker see the same underlying bytes.
// main.js
const sharedBuffer = new SharedArrayBuffer(1024); // 1 KB of shared memory
const view = new Int32Array(sharedBuffer);
view[0] = 42;
const worker = new Worker("worker.js");
worker.postMessage(sharedBuffer);
// worker.js
self.onmessage = (e) => {
const view = new Int32Array(e.data);
console.log(view[0]); // 42 -- same memory!
view[0] = 100; // Main thread will see this change
};
Unlike a normal ArrayBuffer transfer, the main thread retains access to the buffer. Both sides share it.
The problem: data races
Shared memory introduces data races. If two threads read and write the same location without coordination, results become unpredictable.
// Thread A
view[0] = view[0] + 1;
// Thread B (simultaneously)
view[0] = view[0] + 1;
// Expected: view[0] incremented by 2
// Actual: might only increment by 1 (lost update)
The increment is not atomic — it involves a read, a computation, and a write. Another thread can interleave between those steps. This is where Atomics comes in.
Atomics: thread-safe operations
The Atomics object provides static methods that perform atomic (indivisible) operations on SharedArrayBuffer views.
Atomic read and write
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
// Atomic write -- guaranteed to complete before any other thread sees it
Atomics.store(view, 0, 42);
// Atomic read -- guaranteed to see the latest value
const value = Atomics.load(view, 0);
console.log(value); // 42
Atomic add and subtract
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.store(view, 0, 10);
// Atomically add 5 and return the old value
const oldValue = Atomics.add(view, 0, 5);
console.log(oldValue); // 10
console.log(Atomics.load(view, 0)); // 15
// Atomically subtract 3
Atomics.sub(view, 0, 3);
console.log(Atomics.load(view, 0)); // 12
Compare and exchange
Atomics.compareExchange is the foundation for building higher-level synchronization. It atomically checks whether an index holds an expected value and, if so, replaces it.
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.store(view, 0, 10);
// Only update if current value is 10
const old = Atomics.compareExchange(view, 0, 10, 20);
console.log(old); // 10 (was 10, so the swap happened)
console.log(Atomics.load(view, 0)); // 20
// Try again -- current value is now 20, not 10, so swap fails
const old2 = Atomics.compareExchange(view, 0, 10, 30);
console.log(old2); // 20 (was not 10, no swap)
console.log(Atomics.load(view, 0)); // 20
Wait and notify: thread coordination
Atomics.wait and Atomics.notify let threads block and wake each other, similar to condition variables in other languages.
// worker.js -- waiting thread
self.onmessage = (e) => {
const view = new Int32Array(e.data);
console.log("Worker waiting...");
// Block until view[0] is no longer 0
const result = Atomics.wait(view, 0, 0);
console.log("Worker woke up:", result); // "ok"
console.log("Value:", Atomics.load(view, 0));
};
// main.js -- notifying thread
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
const worker = new Worker("worker.js");
worker.postMessage(sab);
setTimeout(() => {
Atomics.store(view, 0, 1);
Atomics.notify(view, 0, 1); // Wake one waiting thread
}, 2000);
Important: Atomics.wait cannot be called on the main thread in browsers (it would block the UI). Use Atomics.waitAsync instead, which returns a promise.
// Main thread -- non-blocking wait
const result = Atomics.waitAsync(view, 0, 0);
result.value.then(() => {
console.log("Value changed:", Atomics.load(view, 0));
});
Building a simple spinlock
A spinlock is a basic mutual exclusion mechanism. While not ideal for performance, it demonstrates how Atomics enable synchronization primitives.
class SpinLock {
constructor(sharedArray, index) {
this.view = sharedArray;
this.index = index;
}
lock() {
// Keep trying until we successfully set 0 -> 1
while (Atomics.compareExchange(this.view, this.index, 0, 1) !== 0) {
// Spin -- in real code you might want to yield or back off
}
}
unlock() {
Atomics.store(this.view, this.index, 0);
}
}
// Usage (in a worker)
const sab = new SharedArrayBuffer(8); // 4 bytes for lock, 4 for data
const lockView = new Int32Array(sab);
const lock = new SpinLock(lockView, 0);
lock.lock();
// Critical section -- only one thread at a time
Atomics.add(lockView, 1, 1);
lock.unlock();
Practical example: parallel sum
Here is a complete example that splits an array across workers and computes a sum in parallel.
// main.js
const WORKER_COUNT = 4;
const DATA_SIZE = 1_000_000;
// Shared buffer: data + one slot per worker for partial sums
const dataBuffer = new SharedArrayBuffer(DATA_SIZE * 4);
const resultBuffer = new SharedArrayBuffer(WORKER_COUNT * 4);
const data = new Int32Array(dataBuffer);
const results = new Int32Array(resultBuffer);
// Fill with test data
for (let i = 0; i < DATA_SIZE; i++) {
data[i] = 1; // Sum should be 1,000,000
}
const chunkSize = Math.ceil(DATA_SIZE / WORKER_COUNT);
let completed = 0;
for (let i = 0; i < WORKER_COUNT; i++) {
const worker = new Worker("sum-worker.js");
worker.postMessage({
dataBuffer,
resultBuffer,
workerIndex: i,
start: i * chunkSize,
end: Math.min((i + 1) * chunkSize, DATA_SIZE),
});
worker.onmessage = () => {
completed++;
if (completed === WORKER_COUNT) {
let total = 0;
for (let j = 0; j < WORKER_COUNT; j++) {
total += Atomics.load(results, j);
}
console.log("Total sum:", total); // 1000000
}
};
}
// sum-worker.js
self.onmessage = (e) => {
const { dataBuffer, resultBuffer, workerIndex, start, end } = e.data;
const data = new Int32Array(dataBuffer);
const results = new Int32Array(resultBuffer);
let sum = 0;
for (let i = start; i < end; i++) {
sum += data[i];
}
Atomics.store(results, workerIndex, sum);
self.postMessage("done");
};
Security requirements
Browsers disabled SharedArrayBuffer after the Spectre vulnerability. To re-enable it, your server must set two headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
You can check availability at runtime:
if (typeof SharedArrayBuffer !== "undefined") {
console.log("SharedArrayBuffer is available");
} else {
console.log("SharedArrayBuffer is not available -- check COOP/COEP headers");
}
Summary
SharedArrayBuffer and Atomics bring true shared-memory parallelism to JavaScript:
SharedArrayBuffercreates memory accessible from multiple threads without copying.Atomics.loadandAtomics.storeensure reads and writes are atomic.Atomics.add,Atomics.sub, andAtomics.compareExchangeprovide lock-free data manipulation.Atomics.waitandAtomics.notifycoordinate threads via blocking and waking.- Security headers (COOP/COEP) are required in browsers.
These APIs are powerful but low-level. For most applications, the message-passing model with postMessage is simpler and less error-prone. Reach for shared memory when you need maximum throughput for CPU-intensive parallel computation.
Related articles
- JavaScript JavaScript Web Workers: Run Code Off the Main Thread
Learn how to use JavaScript Web Workers to run CPU-intensive code off the main thread, keeping your UI responsive with practical examples and patterns.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript Promise Combinators: all, allSettled, race, and any
Master all four JavaScript Promise combinators -- Promise.all, allSettled, race, and any -- with practical patterns for concurrency, timeouts, and resilience.
- JavaScript JavaScript Event Loop, Microtasks, and Macrotasks Explained
Visualize how the JavaScript event loop works -- understand the call stack, microtask queue, macrotask queue, and execution order.