为什么需要原子操作
JavaScript 传统上是单线程执行的,但现代 Web 平台通过 Web Workers 提供了多线程能力。然而,Workers 之间是隔离的,它们通过消息传递进行通信,无法直接共享内存。这在需要高性能并行处理的场景(如数据分析、物理模拟、加密计算)成了瓶颈。
SharedArrayBuffer 和 Atomics API 的出现改变了这一状况。它们允许 JavaScript 在多个线程之间共享内存,提供了真正的并行计算能力。
一、SharedArrayBuffer 基础
1.1 什么是 SharedArrayBuffer
SharedArrayBuffer 是一个表示二进制数据的容器,与 ArrayBuffer 类似,但可以在多个 Agent(主线程和 Workers)之间共享:
// 创建共享缓冲区,8 字节长度
const sharedBuffer = new SharedArrayBuffer(8);
// 在不同线程中访问同一个缓冲区
// Worker 1:
const view = new Int32Array(sharedBuffer);
view[0] = 42;
// Worker 2 (同一 sharedBuffer):
const view = new Int32Array(sharedBuffer);
console.log(view[0]); // 42 — 跨线程读取
1.2 安全问题:Spectre 和 Meltdown
SharedArrayBuffer 最初在 2017 年被引入浏览器,但由于 Spectre 和 Meltdown 漏洞,2018 年初被完全禁用。这两个漏洞允许攻击者利用 CPU 的推测执行机制,从受害进程中读取任意内存。
浏览器重新启用 SharedArrayBuffer 的条件是实现了 Cross-Origin Isolation(跨域隔离),通过设置以下 HTTP 响应头:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
这些头部确保页面不与跨域文档共享浏览上下文,从而防止 Spectre 类攻击。
1.3 检测跨域隔离
if (typeof SharedArrayBuffer !== 'undefined') {
// 检查是否在跨域隔离环境中
if (self.crossOriginIsolated) {
console.log('SharedArrayBuffer is available');
} else {
console.log('SharedArrayBuffer is NOT available — not cross-origin isolated');
}
}
二、Atomics API
2.1 Atomics 的设计目标
Atomics 对象提供了一组静态方法,用于在共享内存上执行原子操作。原子操作保证在多线程环境中,操作是"不可分割"的——不会被其他线程的操作插断。
核心操作类型:
- 加载(Load):原子读取值
- 存储(Store):原子写入值
- 修改(Modify):读-改-写操作(add、sub、and、or、xor)
- 交换(Exchange):原子替换值
- 比较交换(Compare Exchange):条件原子写入
2.2 基础原子操作
const sharedBuffer = new SharedArrayBuffer(8);
const int32View = new Int32Array(sharedBuffer);
// 原子写入
Atomics.store(int32View, 0, 42);
// 原子读取
const value = Atomics.load(int32View, 0);
console.log(value); // 42
// 原子加法:返回旧值
const oldValue = Atomics.add(int32View, 0, 10);
console.log(Atomics.load(int32View, 0)); // 52
// 原子子减:返回旧值
const oldValue2 = Atomics.sub(int32View, 0, 5);
console.log(Atomics.load(int32View, 0)); // 47
2.3 原子交换与比较交换
// 原子交换:用新值替换指定位置的值,返回旧值
const oldValue = Atomics.exchange(int32View, 0, 100);
console.log(oldValue); // 47 (旧值)
console.log(Atomics.load(int32View, 0)); // 100 (新值)
// 原子比较交换(CAS):如果期望值匹配才写入
// compareExchange(buffer, index, expected, replacement)
// 返回:旧值(如果匹配则 replacement,否则返回原值)
const expected = 100;
const result = Atomics.compareExchange(int32View, 0, expected, 200);
console.log(result); // 100 — 匹配,写入成功
// 再次尝试,expected 已经是 200,不匹配
const result2 = Atomics.compareExchange(int32View, 0, 100, 300);
console.log(result2); // 200 — 不匹配,返回当前值,缓冲区不变
CAS 是实现无锁数据结构的核心操作。
2.4 原子位操作
// 原子 AND
Atomics.store(int32View, 0, 0b1111); // 15
Atomics.and(int32View, 0, 0b1100); // 0b1111 & 0b1100 = 0b1100 = 12
// 原子 OR
Atomics.store(int32View, 0, 0b0101); // 5
Atomics.or(int32View, 0, 0b0011); // 0b0101 | 0b0011 = 0b0111 = 7
// 原子 XOR
Atomics.store(int32View, 0, 0b1010); // 10
Atomics.xor(int32View, 0, 0b0110); // 0b1010 ^ 0b0110 = 0b1100 = 12
三、原子等待与通知
3.1 Atomics.wait 与 Atomics.notify
这两个操作提供了一种线程间同步机制:
Atomics.wait(arr, index, value, timeout):在指定位置等待值变化Atomics.notify(arr, index, count):通知等待的线程
// 主线程:等待 Worker 处理完成
const sharedBuffer = new SharedArrayBuffer(8);
const int32View = new Int32Array(sharedBuffer);
// 设置初始值
Atomics.store(int32View, 0, 0);
console.log('Waiting for worker to set value...');
// 等待 int32View[0] 变为非零值,最多等待 1000ms
const result = Atomics.wait(int32View, 0, 0, 1000);
console.log('Wait result:', result); // 'ok', 'not-equal', or 'timed-out'
// Worker 线程:处理完成后通知主线程
Atomics.store(int32View, 0, 1); // 写入完成标志
Atomics.notify(int32View, 0, 1); // 通知 1 个等待的线程
3.2 使用场景
原子等待/通知适用于:
- 生产者-消费者模式:Worker 生产数据,主线程消费
- 屏障同步:等待所有 Worker 完成后再继续
- 锁实现:基于 CAS 和 wait/notify 实现自定义锁
3.3 注意事项
Atomics.wait 只能在 main thread 调用,在 Worker 中调用会抛出错误。Worker 应该使用 Atomics.waitAsync(ES2023):
// Worker 中使用 waitAsync
const result = Atomics.waitAsync(int32View, 0, 0, 1000);
result.value.then(r => console.log('Wait result:', r));
四、实用模式
4.1 简单的计数器
class AtomicCounter {
constructor(initialValue = 0) {
this.buffer = new SharedArrayBuffer(4);
this.view = new Int32Array(this.buffer);
Atomics.store(this.view, 0, initialValue);
}
get value() {
return Atomics.load(this.view, 0);
}
increment() {
return Atomics.add(this.view, 0, 1);
}
decrement() {
return Atomics.sub(this.view, 0, 1);
}
}
// 使用
const counter = new AtomicCounter(0);
counter.increment(); // 返回 0(旧值),当前值 1
counter.increment(); // 返回 1(旧值),当前值 2
console.log(counter.value); // 2
4.2 简单的锁
class SimpleLock {
constructor() {
this.buffer = new SharedArrayBuffer(4);
this.view = new Int32Array(this.buffer);
Atomics.store(this.view, 0, 0); // 0 = unlocked, 1 = locked
}
acquire() {
// 自旋锁:不断尝试获取锁
while (!this.tryAcquire()) {
Atomics.wait(this.view, 0, 1, 100); // 等待 100ms
}
}
tryAcquire() {
return Atomics.compareExchange(this.view, 0, 0, 1) === 0;
}
release() {
Atomics.store(this.view, 0, 0);
Atomics.notify(this.view, 0, 1); // 通知一个等待的线程
}
}
4.3 环形缓冲区
class RingBuffer {
constructor(capacity = 1024) {
this.capacity = capacity;
this.buffer = new SharedArrayBuffer(capacity + 8); // +8 for pointers
this.dataView = new Uint8Array(this.buffer, 8);
this.pointerView = new Int32Array(this.buffer, 0, 2);
}
get readIndex() {
return Atomics.load(this.pointerView, 0);
}
get writeIndex() {
return Atomics.load(this.pointerView, 1);
}
push(data) {
const writeIdx = this.writeIndex;
const nextWriteIdx = (writeIdx + 1) % this.capacity;
const readIdx = this.readIndex;
if (nextWriteIdx === readIdx) {
return false; // Buffer full
}
this.dataView[writeIdx] = data;
Atomics.store(this.pointerView, 1, nextWriteIdx);
return true;
}
pop() {
const readIdx = this.readIndex;
const writeIdx = this.writeIndex;
if (readIdx === writeIdx) {
return null; // Buffer empty
}
const data = this.dataView[readIdx];
const nextReadIdx = (readIdx + 1) % this.capacity;
Atomics.store(this.pointerView, 0, nextReadIdx);
return data;
}
}
五、面试高频考点
考点 1:原子操作的意义
原子操作保证在多线程环境中,操作不会被其他线程的操作分割。要么操作完全完成,要么完全不执行,不会出现"读到一半"的状态。这对于共享状态的数据一致性至关重要。
考点 2:CAS 操作的原理
Compare Exchange 首先读取当前值,与期望值比较。如果相等,则写入新值并返回旧值;如果不等,则不做修改并返回当前值。CAS 是无锁编程的基础,可以实现各种同步原语。
考点 3:SharedArrayBuffer 的安全机制
Spectre/Meltdown 漏洞利用 CPU 推测执行读取不该访问的内存。浏览器的缓解措施是 Cross-Origin Isolation,通过 COOP 和 COEP 头部确保页面隔离。