在 Web Worker 已经普及的今天,前端开发者对多线程编程不再陌生。但 Worker 之间的通信依赖 postMessage 消息传递,数据需要经过序列化和反序列化,遇到大量数据高频交互的场景性能就会成为瓶颈。SharedArrayBuffer 的出现让多个 Worker 可以直接共享同一块内存,但共享内存带来一个经典问题:如果两个线程同时修改同一个位置,数据就会出错。这个时候,Atomics 对象就是解决问题的核心工具。

为什么共享内存必须依赖 Atomics
要理解 Atomics 的必要性,先看一个反例。假设两个线程同时对同一个 Int32Array 的同一位置执行 arr[0] = arr[0] + 1,这个语句在 JavaScript 层面是一条语句,但在 CPU 层面实际分成了读取、加一、写回三个步骤。线程 A 刚读完值 5,还没写回,线程 B 也读到了 5,各自加一后都写回 6,最终结果只加了一次。这就是典型的竞态条件。
除了竞态问题,还有一个容易被忽视的点:JavaScript 引擎和 CPU 都可能对指令重排序。普通的对 TypedArray 的读写操作,编译器可以随意调整顺序以优化性能,而在多线程环境下这种重排可能让另一个线程观察到不符合预期的状态。Atomics 提供的所有操作都保证了两个特性:一是原子性,操作不可分割,其他线程要么看到操作前的值,要么看到操作后的值;二是有序性,Atomics 操作之间不会发生重排序问题,配合 Atomics.load 和 Atomics.store 可以在共享内存上建立同步点。
需要强调的是,Atomics 的所有方法只接受 Int8Array、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、BigInt64Array、BigUint64Array 这些整数类型的 TypedArray,不支持 Float32Array 和普通数组。原因在于 CPU 原子指令通常只覆盖整数操作,浮点数无法保证硬件级别的原子性。
Atomics 的核心方法详解
Atomics 的 API 可以分成三组。第一组是基本的原子读写和修改类操作。Atomics.store(ta, index, value) 和 Atomics.load(ta, index) 分别负责原子写入和读取;Atomics.add、Atomics.sub、Atomics.and、Atomics.or、Atomics.xor 则是原子化的位运算和加减运算,它们都会返回修改前的旧值,这个设计在实现无锁计数器时非常实用。
第二组是交换类操作。Atomics.exchange(ta, index, value) 会把新值写入并返回旧值,整个过程原子完成。Atomics.compareExchange(ta, index, expected, replacement) 则更精细:只有当当前位置的值等于 expected 时才写入 replacement,否则不做任何修改,无论成败都返回当前实际值。它是实现自旋锁、无锁算法的基石,下面是一个简单的自旋锁实现:
// 使用 Atomics.compareExchange 实现自旋锁
// lock 值为 0 表示未锁定,1 表示已锁定
const sab = new SharedArrayBuffer(4);
const lock = new Int32Array(sab);
function acquireLock() {
// 不断尝试把 0 改成 1,成功说明抢到了锁
while (Atomics.compareExchange(lock, 0, 0, 1) !== 0) {
// 抢锁失败,短暂等待后重试,避免空转浪费 CPU
Atomics.wait(lock, 0, 1, 10);
}
}
function releaseLock() {
// 释放锁并唤醒一个等待线程
Atomics.store(lock, 0, 0);
Atomics.notify(lock, 0, 1);
}第三组是等待通知机制,也就是 Atomics.wait 和 Atomics.notify。这一组方法让 JavaScript 具备了类似操作系统层面的条件变量能力。Atomics.wait(ta, index, expected, timeout) 会检查指定位置的值是否等于 expected,如果相等就阻塞当前线程直到被唤醒或超时;如果不相等则立即返回字符串 not-equal。Atomics.notify(ta, index, count) 用来唤醒在该位置上等待的线程。要注意的是,wait 只能在 Worker 线程中调用,主线程调用会直接抛出异常,因为阻塞主线程会导致页面卡死。
实战:用 Atomics 实现生产者消费者模型
下面通过一个完整的例子演示 Atomics 在实际多线程协作中的用法。场景是主线程往共享缓冲区写任务,Worker 线程消费任务。为了简单起见,用一个位置作为信号量来协调双方。
首先是主线程的代码:
<script>
const sab = new SharedArrayBuffer(8);
const control = new Int32Array(sab);
control[0] = 0; // 0 表示空闲,1 表示有数据
const worker = new Worker('consumer.js');
worker.postMessage(sab);
function produce(value) {
// 等待消费者处理完上一条数据
Atomics.wait(control, 0, 1);
control[1] = value; // 写入数据
Atomics.store(control, 0, 1); // 原子标记为有数据
Atomics.notify(control, 0); // 唤醒消费者
}
produce(42);
</script>然后是 consumer.js 中 Worker 端的代码:
// consumer.js - Worker 消费者
let control;
self.onmessage = (e) => {
control = new Int32Array(e.data);
consumeLoop();
};
function consumeLoop() {
while (true) {
// 等待数据就绪,若 control[0] 不为 0 则立即返回 not-equal
const result = Atomics.wait(control, 0, 0);
if (result === 'ok' || result === 'not-equal') {
const value = Atomics.load(control, 1); // 读取数据
Atomics.store(control, 0, 0); // 标记为已消费
Atomics.notify(control, 0); // 通知生产者可以继续
console.log('消费到数据:', value);
}
}
}这个例子体现了 Atomics 的典型用法:数据的可见性通过 Atomics.store 和 Atomics.load 保证,线程间的协调通过 wait 和 notify 完成。相比 postMessage 每次传递都要拷贝数据,共享内存加原子操作的方式在高频小数据交互下吞吐量可以提升数倍,特别适合音频处理、图像编解码、科学计算这类需要大量数据交换的场景。
使用时的限制与注意事项
SharedArrayBuffer 的可用性有一个绕不开的前提:出于 Spectre 漏洞的安全考虑,主流浏览器要求页面必须处于跨域隔离状态才能使用它。具体来说,服务器需要返回两个响应头:Cross-Origin-Opener-Policy: same-origin 和 Cross-Origin-Embedder-Policy: require-corp,可以通过 crossOriginIsolated 这个全局变量检测是否满足条件。本地开发时这个限制经常让人卡住,值得提前配置好。
另外几点实践建议:一是能用整数就用整数,Atomics 不支持浮点类型,如果必须传浮点数可以用 ArrayBuffer 和 Float64Array 共享同一块内存,通过整数区域做控制信号,浮点区域做数据载体;二是不要滥用 wait 的超时参数做轮询,优先依赖 notify 精确唤醒;三是 Node.js 环境同样支持这套 API,worker_threads 模块配合 SharedArrayBuffer 的用法和浏览器端完全一致,代码可以两边复用。
总结一下,Atomics 在 SharedArrayBuffer 体系中扮演的角色就是秩序的维护者。共享内存解决了性能问题,而 Atomics 通过原子性和可见性保证解决了正确性问题,二者缺一不可。掌握这套机制,就掌握了在 JavaScript 中做真正意义上多线程共享内存编程的钥匙。
AtomicsSharedArrayBufferWeb Worker修改时间:2026-09-15 09:10:34