首页
学习
活动
专区
工具
TVP
发布

同步锁基本原理与实现

为充分利用机器性能,人们发明了多线程。但同时带来了线程安全问题,于是人们又发明了同步锁。

这个问题自然人人知道,但你真的了解同步锁吗?还是说你会用其中的上锁与解锁功能?

今天我们就一起来深入看同步锁的原理和实现吧!

一:同步锁的职责

同步锁的职责可以说就一个,限制资源的使用(线程安全从属)。

它一般至少会包含两个功能: 1. 给资源加锁;2. 给资源解锁;另外,它一般还有 等待/通知 即 wait/notify 的功能;

同步锁的应用场景:多个线程同时操作一个事务必须保证正确性;一个资源只能同时由一线程访问操作;一个资源最多只能接入k的并发访问;保证访问的顺序性;

同步锁的实现方式:操作系统调度实现;应用自行实现;CAS自旋;

同步锁的几个问题:

为什么它能保证线程安全?

锁等待耗CPU吗?

使用锁后性能下降严重的原因是啥?

二:同步锁的实现一:lock/unlock

其实对于应用层来说,非常多就是 lock/unlock , 这也是锁的核心。

AQS 是java中很多锁实现的基础,因为它屏蔽了很多繁杂而底层的阻塞操作,为上层抽象出易用的接口。

我们就以AQS作为跳板,先来看一下上锁的过程。为不至于陷入具体锁的业务逻辑中,我们先以最简单的 CountDownLatch 看看。

重点1,我们看看上锁过程,即 await() 的调用。

如上,上锁过程是比较简单明了的。加入一队列,然后由操作系统将线程调出。(那么操作系统是如何把线程调出的呢?有兴趣自行研究)

重点2. 解锁过程,即 countDown() 调用

重要3. 线程解锁的传播性?

因为从上一节的讲解中,我们看到,当用户调用 countDown 时,仅仅是让操作系统唤醒了 head 的下一个节点线程或者最近未取消的节点。那么,从哪里来的所有线程都获取了锁从而运行呢?

其实是在 获取锁的过程中,还有一点我们未看清:

到此,我们明白了它是怎么做到一个锁释放,所有线程可通行的。也从根本上回答了我们猜想,所有线程同时并发运行。然而并没有,它只是通过唤醒传播性来依次唤醒各个等待线程的。从绝对时间性上来讲,都是有先后关系的。以后可别再浅显说是同时执行了哟。

三、 锁的切换:wait/notify

上面看出,针对一个lock/unlock 的过程还是很简单的,由操作系统负责大头,实现代码也并不多。

但是针对稍微有点要求的场景,就会进行条件式的操作。比如:持有某个锁运行一段代码,但是,运行时发现某条件不满足,需要进行等待而不能直接结束,直到条件成立。即所谓的 wait 操作。

乍一看,wait/notify 与 lock/unlock 很像,其实不然。区分主要是 lock/unlock 是针对整个代码段的,而 wait/notify 则是针对某个条件的,即获取了锁不代表条件成立了,但是条件成立了一定要在锁的前提下才能进行安全操作。

那么,是否 wait/notify 也一样的实现简单呢?比如java的最基础类 Object 类就提供了 wait/notify 功能。

我们既然想一探究竟,还是以并发包下的实现作为基础吧,毕竟 java 才是我们的强项。

本次,咱们以  ArrayBlockingQueue#put/take 作为基础看下这种场景的使用先。

ArrayBlockingQueue 的put/take 特性就是,put当队列满时,一直阻塞,直到有可用位置才继续运行下一步。而take当队列为空时一样阻塞,直到队列里有数据才运行下一步。这种场景使用锁主不好搞了,因为这是一个条件判断。put/take 如下:

看起来相当简单,完全符合人类思维。只是,这里使用的两个变量进行控制流程 notFull,notEmpty. 这两个变量是如何进行关联的呢?

在这之前,我们还需要补充下上面的例子,即 notFull.await(), notEmpty.await(); 被阻塞了,何时才能运行呢?如上代码在各自的入队和出队完成之后进行通知就可以了。

是不是超级好理解。是的。不过,我们不是想看 ArrayBlockingQueue 是如何实现的,我们是要论清 wait/notify 是如何实现的。因为毕竟,他们不是一个锁那么简单。

接下来,我们要带着几个疑问来看这个 Condition 的对象:

1. 它的 wait/notify 是如何实现的?

2. 它是如何与互相进行联系的?

3. 为什么 wait/notify 必须要在外面的lock获取之后才能执行?

4. 它与Object的wait/notify 有什么相同和不同点?

能够回答了上面的问题,基本上对其原理与实现也就理解得差不多了。

重点1. wait/notify 是如何实现的?

我们从上面可以看到,它是通过调用 await()/signal() 实现的,到底做事如何,且看下面。

总结一下 wait 的逻辑:

1. 前提:自身已获取到外部锁;

2. 将当前线程添加到 ConditionQueue 等待队列中;

3. 释放已获取到的锁;

4. 反复检查进入等待,直到当前节点被移动到同步队列中;

5. 条件满足被唤醒,重新竞争外部锁,成功则返回,否则继续阻塞;(外部锁是同一个,这也是要求两个对象必须存在依赖关系的原因)

6. wait前线程持有锁,wait后线程持有锁,没有一点外部锁变化;

重点2. 厘清了 wait, 接下来,我们看 signal() 通知唤醒的实现:

总结一下,notify 的功能原理如下:

1. 前提:自身已获取到外部锁;

2. 转移下一个等待队列的节点到同步队列中;

3. 如果遇到下一节点被取消情况,顺延到再下一节点直到为空,至多转移一个节点;

4. 正常情况下不做线程的唤醒操作;

所以,实现 wait/notify, 最关键的就是维护两个队列,等待队列与同步队列,而且都要求是在有外部锁保证的情况下执行。

到此,我们也能回答一个问题:为什么wait/notify一定要在锁模式下才能运行?

因为wait是等待条件成立,此时必定存在竞争需要做保护,而它自身又必须释放锁以使外部条件可成立,且后续需要做恢复动作;而notify之后可能还有后续工作必须保障安全,notify只是锁的一个子集。。。

四、通知所有线程的实现:notifyAll

有时条件成立后,可以允许所有线程通行,这时就可以进行 notifyAll, 那么如果达到通知所有的目的呢?是一起通知还是??

以下是 AQS 中的实现:

可以看到,它是通过遍历所有节点,依次转移等待队列到同步队列(通知)的,原本就没有人能同时干几件事的!

本文从java实现的角度去解析同步锁的原理与实现,但并不局限于java。道理总是相通的,只是像操作系统这样的大佬,能干的活更纯粹:比如让cpu根本不用调度一个线程。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20210324A018YQ00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券