深入理解并发编程和归纳总结(上)

AbstractQueuedSynchronizer

学习 AQS 的必要性

队列同步器 AbstractQueuedSynchronizer(以下简称同步器或 AQS),是用 来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状 态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。

AQS 使用方式和其中的设计模式

AQS 的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管 理同步状态,在 AQS 里由一个 int 型的 state 来代表这个状态,在抽象方法的实 现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法 (getState()、setState(int newState)和 compareAndSetState(int expect,int update)) 来进行操作,因为它们能够保证状态的改变是安全的。
在这里插入图片描述
在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS 自身没有 实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义 同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地 获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步 器。可以这样理解二者之间的关系:
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线 程并行访问),隐藏了实现细节;
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、 线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者 所需关注的领域。
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步 组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者 重写的方法。

模板方法模式

同步器的设计基于模板方法模式。模板方法模式的意图是,定义一个操作中 的算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改 变一个算法的结构即可重定义该算法的某些特定步骤。我们最常见的就是 Spring 框架里的各种 Template。
实际例子
我们开了个蛋糕店,蛋糕店不能只卖一种蛋糕呀,于是我们决定先卖奶油蛋 糕,芝士蛋糕和慕斯蛋糕。三种蛋糕在制作方式上一样,都包括造型,烘焙和涂 抹蛋糕上的东西。所以可以定义一个抽象蛋糕模型。
在这里插入图片描述
然后就可以批量生产三种蛋糕
在这里插入图片描述
在这里插入图片描述
这样一来,不但可以批量生产三种蛋糕,而且如果日后有扩展,只需要继承 抽象蛋糕方法就可以了,十分方便,我们天天生意做得越来越赚钱。突然有一天, 我们发现市面有一种最简单的小蛋糕销量很好,这种蛋糕就是简单烘烤成型就可 以卖,并不需要涂抹什么食材,由于制作简单销售量大,这个品种也很赚钱,于 是我们也想要生产这种蛋糕。但是我们发现了一个问题,抽象蛋糕是定义了抽象 的涂抹方法的,也就是说扩展的这种蛋糕是必须要实现涂抹方法,这就很鸡儿蛋 疼了。怎么办?我们可以将原来的模板修改为带钩子的模板。
在这里插入图片描述
做小蛋糕的时候通过 flag 来控制是否涂抹,其余已有的蛋糕制作不需要任何 修改可以照常进行。
在这里插入图片描述

AQS 中的方法

模板方法

实现自定义同步组件时,将会调用同步器提供的模板方法,
在这里插入图片描述
这些模板方法同步器提供的模板方法基本上分为 3 类:独占式获取与释放同 步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。

可重写的方法

在这里插入图片描述

访问或修改同步状态的方法

重写同步器指定的方法时,需要使用同步器提供的如下 3 个方法来访问或修 改同步状态。
•getState():获取当前同步状态。
•setState(int newState):设置当前同步状态。
•compareAndSetState(int expect,int update):使用 CAS 设置当前状态,该方 法能够保证状态设置的原子性。

CLH 队列锁

CLH 队列锁即 Craig, Landin, and Hagersten (CLH) locks。 CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程 仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束 自旋。
当一个线程需要获取锁时:

  1. 创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred 表示对其前驱结点的引用
    在这里插入图片描述

  2. 线程 A 对 tail 域调用 getAndSet 方法,使自己成为队列的尾部,同时获取 一个指向其前驱结点的引用 myPred

  3. 在这里插入图片描述
    线程 B 需要获得锁,同样的流程再来一遍
    在这里插入图片描述
    3.线程就在前驱结点的 locked 字段上旋转,直到前驱结点释放锁(前驱节点 的锁值 locked == false)
    4.当一个线程需要释放锁时,将当前结点的 locked 域设置为 false,同时回收 前驱结点
    在这里插入图片描述
    如上图所示,前驱结点释放锁,线程 A 的 myPred 所指向的前驱结点的 locked 字段变为 false,线程 A 就可以获取到锁。
    CLH 队列锁的优点是空间复杂度低(如果有 n 个线程,L 个锁,每个线程每 次只获取一个锁,那么需要的存储空间是 O(L+n),n 个线程有 n 个 myNode, L 个锁有 L 个 tail)。CLH 队列锁常用在 SMP 体系结构下。
    Java 中的 AQS 是 CLH 队列锁的一种变体实现。

ReentrantLock 的实现

锁的可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞, 该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程, 如果是,则再次成功获取。
2)锁的最终释放。线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其 他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示 当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已 经成功释放。
nonfairTryAcquire 方法增加了再次获取同步状态的处理逻辑:通过判断当前 线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请 求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。同步状态表 示锁被一个线程重复获取的次数。
如果该锁被获取了 n 次,那么前(n-1)次 tryRelease(int releases)方法必须返回 false,而只有同步状态完全释放了,才能返回 true。可以看到,该方法将同步状 态是否为 0 作为最终释放的条件,当同步状态为 0 时,将占有线程设置为 null, 并返回 true,表示释放成功。

公平和非公平锁

ReentrantLock 的构造函数中,默认的无参构造函数将会把 Sync 对象创建为 NonfairSync 对象,这是一个“非公平锁”;而另一个构造函数 ReentrantLock(boolean fair)传入参数为 true 时将会把 Sync 对象创建为“公平锁” FairSync。
nonfairTryAcquire(int acquires)方法,对于非公平锁,只要 CAS 设置同步状态 成功,则表示当前线程获取了锁,而公平锁则不同。tryAcquire 方法,该方法与 nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的 判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此 需要等待前驱线程获取并释放锁之后才能继续获取锁。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gujunhe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值