今天学习操作系统——信号量、PV 操作、临界区。
看那个 P 操作和 V 操作的流程图,我忽然愣住了:
这不就是我 Java 代码里的 synchronized 和 ReentrantLock 吗?
再一想,分布式场景下的 Redis 锁、ZooKeeper 锁,不也是 PV 操作的“网络版”?

        我是Evan,一个刚啃完 PV 操作的 Java+AI 学生。
        今天这篇博客,我不想背概念,而是从 Java 开发的视角,把操作系统的信号量和我们天天写的锁、分布式互斥对应起来。你会发现:PV 操作是单机版的“锁原型”,而分布式锁只是把它搬到了网络世界

一、先搞懂三个词:临界区、P 操作、V 操作

1.1 临界区(Critical Section)

就是一段 同一时刻只允许一个线程执行的代码。比如:

// 这段代码就是临界区
synchronized (this) {
    count++;  // 多线程环境下必须互斥
}
1.2 P 操作(Proberen,荷兰语“尝试”)
  • 作用:进入临界区之前检查资源是否可用

  • 如果资源计数 > 0,则减 1,继续执行;

  • 如果资源计数 = 0,则当前线程阻塞,进入等待队列。

1.3 V 操作(Verhogen,“增加”)
  • 作用:退出临界区后释放资源

  • 把资源计数加 1,并唤醒一个等待的线程。

一句话:P 是“拿锁”,V 是“放锁”。

二、PV 操作和 Java 锁的惊人相似

2.1 二进制信号量 → synchronized

二进制信号量只有 0 和 1,就是一把互斥锁。
它的 P/V 操作完美对应 synchronized 的底层实现:

// 信号量版(Java 中的 Semaphore)
Semaphore mutex = new Semaphore(1);
mutex.acquire();  // P 操作
try {
    // 临界区
    count++;
} finally {
    mutex.release(); // V 操作
}

// synchronized 版
synchronized (this) {
    count++;
}

本质上,synchronized 就是 JVM 帮你管理的二进制信号量。

2.2 计数信号量 → 限流器 / 连接池

计数信号量允许 N 个线程同时进入。这在 Java 开发中太常见了:

// 最多允许 10 个线程同时访问
Semaphore sem = new Semaphore(10);

sem.acquire();  // P 操作
try {
    // 调用第三方 API,限制并发数
} finally {
    sem.release(); // V 操作
}

        这就是限流的雏形。在知识汇秒杀系统中,我们用 Semaphore 控制同时处理订单的线程数,防止数据库被打垮。

三、PV 操作和分布式锁:只是从“单机内存”搬到了“网络”

PV 操作依赖的是共享内存(信号量对象在内核中)。
分布式环境下,没有共享内存了,怎么办?把“信号量”放到一个外部服务里

一个 Redis 分布式锁的 PV 映射

// P 操作(获取锁)
while (!redis.setnx("lock_key", "thread_id")) {
    Thread.sleep(10); // 自旋等待
}

// 临界区
transferMoney();

// V 操作(释放锁)
redis.del("lock_key");

        这不就是网络版的 PV 吗?
        区别在于:单机 PV 的阻塞是高效的内核挂起;而分布式锁的阻塞往往是用户态自旋轮询,开销更大。

四、一个让你印象深刻的对比表格

五、我在 AI 项目中用“PV 思想”解决过的实际问题

        在智答知识库系统中,我们需要同时处理多个用户的文档上传 + 向量化。
        Python 的 FastAPI 服务用到了 asyncio.Semaphore 控制同时进行的 Embedding 请求数(防止显卡 OOM)。

# Python 版信号量(和 PV 完全一致)
sem = asyncio.Semaphore(5)

async def embed_doc(text):
    async with sem:  # 相当于 P 操作
        result = await call_ollama(text)  # 临界区
    # V 操作自动执行

而在 Java 端,我们用 Redisson 的分布式锁来保证同一个文档不会被两个线程同时解析

RLock lock = redisson.getLock("doc_lock_" + docId);
lock.lock();  // 分布式 P 操作
try {
    parseDocument(doc);
} finally {
    lock.unlock();  // 分布式 V 操作
}

        看到没?从 Dijkstra 的信号量,到 Redis 的分布式锁,五十年来,互斥的思想从来没变过,只是实现的地点和形式变了。

        我们知道 PV 操作中的 P 操作在资源不足时会让当前进程阻塞(进入等待队列,不消耗 CPU)。但是在分布式锁的实现中(比如 Redis 的 setnx),如果锁被占用,我们通常会让线程自旋重试定时轮询,这会造成 CPU 空转。
        问题:为什么分布式锁很难实现像单机 PV 那样高效的“阻塞等待”?如果让你设计一个理想的分布式阻塞锁,你会怎么做?

        欢迎在评论区分享你的设计思路 —— 下一篇我会聊聊 “分布式锁的阻塞实现:基于 ZooKeeper 的临时顺序节点与 watch 机制”。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐