微操征霸
351.42M · 2026-02-04
这篇小记偏向于理论,即 Java 底层是怎么实现互斥同步
锁是信号量的一个子集,目的就是通过 PV 操作控制信号量从而保护临界区中的临界资源
信号量是锁的超集,而PV操作是操作信号量的原语。锁(互斥锁)是信号量的一种特殊用法(初始值为1的信号量)。它们共同的目标是为了保护临界区,从而实现安全地访问临界资源。PV 操作的作用就是为了完成同步和互斥这两个功能
PV 操作是 OS 自带的原语操作,内核才有防止中断的权限,并发和并行是产生数据不同步的根本原因,PV 操控通过控制信号量来互斥地访问临界区中的临界资源,而锁是信号量的一个子集
进程之间为什么要实现互斥:不同进程之间可能会共享某项资源。
进程之间为什么要实现同步:不同进程之间的操作可能会存在先后关系
临界资源被存放在临界区中,OS 可以通过访问临界区去访问对应的临界资源
临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。
属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。
每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问。
多个进程涉及到同一个临界资源的的临界区称为相关临界区。
使用临界区时,一般不允许其运行时间过长,只要运行在临界区的进程还没有离开,其他所有进入此临界区的进程都会被挂起而进入阻塞状态,并在一定程度上影响程序的运行性能。
临界资源一般被放置在内存中,在一台计算机中的处理机读取的是同一个内存。
同步机制应当遵循的准则:
原语操作指的是一系列操作从开始执行到结束都不允许被中断
并发和并行的区别仅在于“是否拥有多个处理机”
从宏观来看,并发与并行属于同时运行的,多个进程之间处于异步,没有区别
从微观来看:
如下图:
并行
X 轴是时间;Y 轴是进程
并发
X 轴是时间;Y 轴是进程
并发就是一个处理器去分神处理多个进程
并行就是多个处理器专一去处理多个进程
来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。
宏观角度来看,都没了三个馒头,但从微观的角度看就不一样了
它本质上就是一个整数变量,用来记录某项资源的数量。
信号量是 OS 提供的一种机制,用于表示临界资源的数量
比如 int count = 5
这是对信号量进行操作的唯一途径(原语):
P (Proberen = 尝试/申请)--- 申请一个资源,如果资源不够就阻塞等待(进入阻塞队列)
P操作的底层,是申请并获得了对“信号量”这个共享数据结构的独占修改权。利用这个独占权,它原子地完成了对资源状态的判断和更新,从而决定了当前进程是否有权访问真正的临界资源
V (Verhogen = 增加/释放) --- 释放一个资源,如果有进程在等待该资源,则唤醒一个进程
V操作的底层则是返还了“信号量”这个共享数据结构,在“信号量”这个共享数据结构被更新后,如果当前“信号量”仍为负数(代表当前有其他进程正在请求这个共享数据结构,并处于阻塞状态)就将阻塞队列的头进程唤醒,它会进入就绪态
锁是一种特殊的信号量,它的资源只有 1(即:要么是 0,要么是 1)。
所以,互斥 = 初始值为 1 的信号量。
在 Java 中,ReentrantLock 就是把 PV 操作封装成了 lock() 和 unlock() 方法给程序员使用。
锁(Lock)不是资源本身,锁是访问资源的“入场券”。
在 Java 中可以使用 Lock.lock 对一块地方上锁(其实是 JVM 向 OS 申请了一块互斥空间/临界资源),在 Java 中,变量可以被放置到临界资源区中,这样子来实现锁
例如
// 1. 【抢钥匙】
// 这一步不是申请资源,而是“申请访问资源的权限”。
// 如果拿到钥匙,就进门;拿不到,就在门口排队(阻塞)。
lock.lock();
try {
// 2. 【临界区 (Critical Section)】
// 只有拿到了钥匙的线程,才能运行这几行代码。
// 在这里,你可以安全地操作“临界资源”(比如 map.put, count++)。
// 此时,别人进不来,因为钥匙在你兜里。
map.put("key", 1);
} finally {
// 3. 【还钥匙】
// 这一步不是把资源还给 OS(厕所还在),而是“归还权限”。
// 把钥匙挂回墙上,通知门口排队的人:“我用完了,你们抢吧”。
lock.unlock();
}
其实 Java 在 lock 的底层就已经实现了阻塞了,那么这个时候就有人要问了,老师老师,那我们(Condition)呢?
一句话总结:阻塞 不等于 释放锁。只有 Condition.await() 才能做到 “在阻塞的同时释放锁”。
把话说得更日常一点:你在食堂吃饭,一般来讲就是 先抢位置(抢锁) -> 去打饭(调用资源) -> 吃饭(使用资源) -> 离开(释放资源和锁) 这一个过程
lock 相当于你抢了一个位置,然后在排打饭队伍,快到你了告诉你:小伙子不好意思啊,饭没了,你得等一下了,condition 的做法是你先把座位放开,让其他已经打了饭同学可以坐,然后你去等阿姨跟你说,饭好了,然后你再去抢座位,抢饭。
如果没有 condition,相当于阿姨跟你说没饭了,你说:那没事儿,我等等,但是座位还是你的啊,其他要吃饭的人就没座位了,那不就是占着茅坑不拉屎了吗?
在 JDK 编写过程,开发者就已经给每一个对象上了一把隐式锁
隐式锁可以通过添加 synchronized 关键字开启
简单来说,之所以基本数据类型不能加锁的原因是:基本数据类型的大小是固定的,没有多余的空间去放锁头
由多个并发进程因争夺系统资源而产生相互等待的现象
死锁产生的四个必要条件
预防死锁:可以通过破坏死锁产生的4个必要条件来 预防死锁
死锁避免:在使用前进行判断,只允许不会产生死锁的进程申请资源;
在 Java 中,锁是 JDK 底层请求 OS 对临界区中的临界资源进行 PV 操作的实际操作,对于多线程和高并发来讲十分重要。
锁是一种同步机制,用于在存在资源竞争的环境中,强制限制对资源的访问顺序。它可以被理解为一个“令牌”或“许可证”,谁拿到了这个令牌,谁就有权访问被保护的资源。
锁的逻辑围绕着两个基本操作:获取锁 和 释放锁,以及它们之间的代码区域——临界区。