AQS原理分享-初学可读

488 阅读4分钟

0. 概述

上篇文章面向接触过一点AQS的同学,本篇文章面向初次接触AQS的同学,仅图解原理模型,不贴源码。如果对AQS,CAS有基本了解,但不清楚具体原理,可以跳过本文,翻阅《AQS原码分享》。

1.0 什么是CAS?

思考:以往我们使用什么方法处理同步代码?锁!对的,但是锁带来的开销很大,也影响性能,于是大神Lea另辟蹊径,写了个不使用锁也可以实现同步的算法-CAS。 Compare and sawp,比较的同时交换,晦涩难懂,白话文表述就是更新数据的时候比较数据。依然抽象!嗯!!! CAS有三个变量,以a=10更新成a=20为例:

第一个变量:想要更新的数据,即originValue = 10;

第二个变量:更新之后的数据:即newValue = 20;

第三个变量:存储originValue的地址,例如1x0001。

第一步:将1x0001存储的数据与originValue相比,判断是否相等;

第二步:如果是,1x0001存储的数据更新成newValue,如果不是,啥都不做。

注意:第一步和第二步在硬件级别上做到了原子性,两步其实是一步,不存在线程安全问题。

这种做法称为乐观锁,数据库层面基于版本号或时间戳实现的乐观锁思想与此一致

2.0 AQS架构-参照下图蓝色元素

CAS非常重要,因为AQS底层争夺锁全是CAS。深入了解一个东西,要从它的结构开始。

一个同步变量-state(这是女神闺蜜,她会告诉你校花有没有男朋友)

state>0说明有线程持有锁,state=0说明当前锁无线程持有

一个重入标志引用-exclusiveOwnerThread(这是女神男朋友)

exclusiveOwnerThread存储当前持有锁的线程对象,不过该变量只有独占式锁才有

一条等待队列-LCH双向链表(这是条备胎队伍)

LCH头尾不存数据,其他节点存储没有争夺到锁的线程,队列里的线程存在两种状态,头节点的后继节点自旋争夺锁,但并不是一直保持自旋,达到一定条件就阻塞;其他节点依法阻塞。

2.1 独占式(以下讲解基于公平锁)

2.1.1 加锁三步走

第一步:判断当前state状态(询问女神闺蜜),state>0说明有线程持有锁(人家有男朋友了),state=0说明当前锁无线程持有(刚刚结束恋情,空窗期);

第二步:在state>0的时候,基于CAS算法插入队尾(想成为备胎,看你本事);state=0可以直接获取锁,这个过程也是CAS算法,防止此时多个线程同时访问,方式加锁(n个备胎一起追求女神);

第三步:如果成功加入同步队列队尾,马上自旋,这过程中若立即获取到同步状态(前置结点为head并且尝试获取同步状态成功)则可以直接执行,若无法立即获取到同步状态则会将前继结点置为SIGNAL状态同时自身通过LockSupport.park()方法进入阻塞状态,等待LockSupport.unpark()方法唤醒

思考:这种自旋和加锁阻塞有什么区别?

自旋是一个简单的循环夹带CAS的过程,CAS的过程直接操作内存,速度非常快,而唤醒线程CPU的代价比自旋大。理想情况下,我们操作同步代码的时候尽量少逻辑,让线程快速执行完同步代码,有可能某些并发有点高的地方,同步代码执行非常快,如果频繁加锁的话,带来的性能损耗也是很大的,采取这种先试着加锁,不行再入队列,再试着加锁的逻辑,有效的提升性能,因为队列里最多两个线程自旋,这比单纯的全部阻塞好一些。值得一提的是,高并发下大量线程自旋消耗着大量资源。

2.1.2 解锁两种情况

持有锁的线程重置state到0即完成解锁。为什么要重置到0,因为独占式允许重入,其中state变量随着重入的次数加1,解锁的次数-1。当加锁的时候,会判断exclusiveOwnerThread是否等于当前线程,如果是,说明当前线程本身持有锁,可以执行同步代码,不需要再争夺锁。解锁的时候也会判断exclusiveOwnerThread是否等于当前线程,且这次解锁之后state是否等于0。解锁成功,唤醒节点第二个节点争夺锁。(思考一下,这里还需要用CAS抢锁吗,不是说只告诉第二个备胎女神恢复单身了吗?答案是要的,别忘了,此时state=0,还有一群还没入队的备胎会跟你抢女神!!!骚年,努力吧!)

2.2 共享式

共享式与独占式最大区别是,独占式解锁唤醒第二个节点,共享式唤醒所有节点。。。