AQS源码分析--前置知识准备

在阅读AQS源码之前,需要先了解一部分知识点,了解这些后有助于阅读AQS。

并发与并行

并发:如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统;即:有多个线程同时处于运行状态且能被cpu执行
并行:“并行”概念是“并发”概念的一个子集;单核cpu无法做到并行;多核cpu对并发的几个线程同时运行,则这几个线程处于并行状态

这是我从知乎某个回答里复制过来的,其他回答的核心也都差不多。可以理解,并行其实是并发的一个子集,在同一时刻,多核cpu同时执行多个任务,这既是并行。
不过,并发也不一定存在并行。如果是单核的cpu是无法做到同时执行多个任务的。就算有多核的cpu,也无法一直并行任务,可能存在某些时候,某些任务是交替执行的。
即A任务被某核cpu执行n毫秒,然后B任务抢占时间片由该cpu执行。
通常的并发情况是,m核cpu某个时刻执行n(n<=m)个任务,这n个任务是并行状态。下一时刻,这n个任务之中的几个执行完,空闲的cpu执行其他任务,这是交替执行。

CAS

《Java并发编程实战》中对CAS的解释:

CAS(CompareAndSwap):比较并交换。
CAS包含了3个操作数----需要读写的内存位置V,进行比较的值A和拟写入的新值B。仅且仅当V的值等于A时,CAS才会通过原子方式用新值B更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。(这种变化形式被称为比较并设置,无论操作是否成功都会返回。)
CAS的含义是:“我认为V的值一个为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少。”
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都将失败。然而,失败的线程并不会被挂起(这与获取锁的情况不同:当获取锁失败后,线程将挂起),而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争CAS时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复的操作,也或者不执行任何操作。
CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后通过CAS以原子方式将V的值由A变成B(只要在这期间没有任何其他线程将V的值修改为其他值)。由于CAS能检测到来自其它线程的干扰,因此即使不使用锁也能够实现原子的读-改-写操作

从马士兵老师的视频中截取的:
在这里插入图片描述注意:当要去修改值的时候,比较原先的值E和当前新值N的时候如果两者不相等,则修改失败,并这个线程会将当前位置的新值读取出来返回,再次由E接收

ABA问题
图中,在比较预期值E和新值N结果为相等后,把值更新为V之前,会产生一个ABA问题。
什么时ABA问题?引用马老师的比喻:

你的女朋友和你分手了,经历了别的男人,后来又复合了,你怎么知道你的女朋友还是你原来的女朋友?真是个扎心又深刻的例子!!!

如何解决ABA问题:
每次修改的时候,不只更新值,也更新一个版本号,这样就算原先的值A改为B再被修改会A,版本号不同了,会使得其他线程修改的时候更新失败,从而避免ABA问题。

一个问题
那么有没有这样一个可能:一个线程去改这个值的时候,这个值被其他线程改掉了,这不就无法保证一致性了么??
这个问题必然是要看C++源码的了:
在这里插入图片描述
cmpxchg(Compare And Exchange):这是一条CAS操作对应的汇编指令,是硬件级别的。这不是一个非原子的指令,所以如何保证多个线程之间改变值的一致性呢?
注意,cmpxchg之前有一个LOCK_IF_MP,表示如果是多核cpu,则lock一下。这表示,当一个cpu在执行cmpxchg指令对某一块内存操作的时候,其他cpu不允许打断

CAS(CompareAndSwap)与 CompareAndSet
compareAndSet是API,其底层调用compareAndSwap(sun包或者native c++)进行实现
AQS中用到了很多通过CAS实现的API:compareAndSetState、compareAndSetTail等

自旋锁

来了解一下什么是自旋锁

自旋

没有竞争到锁的线程一直循环,占用cpu不断进行CAS操作,判断锁是否被释放,如果获取到锁则跳出循环
伪代码:

private void lock(){
	while(!tryLock()){
	    //do something
	}
}
  • 优点:如果锁的竞争不激烈,且持有锁的线程占用锁的时间非常短暂,自旋锁能大幅提升性能,因为自旋消耗会小于线程阻塞挂起后被再次唤醒造成的消耗
  • 缺点:如果锁竞争激烈,或者持有锁的线程长时间占用锁,那么其他需要获取该锁的线程一直占用cpu进行自旋,导致:
    1.线程自旋的消耗大于线程阻塞挂起再唤醒的消耗
    2.其他不竞争该锁的线程占用cpu的时间变少,造成资源的浪费

yield+自旋

未持有锁的线程执行yield方法让出cpu资源,可以减少cpu的空转;但是yield只是让出cpu,可能下次cpu调度还是运行此线程。
比如只有两个线程,一个获取了锁执行同步代码,另一个执行yield让出cpu,但是cpu还是继续调度第二个线程,执行到yield继续让出cpu,循环往复;
再想象这种情况:假设有100个线程,1个获取了锁,另外99个反复的yield+自旋(t1执行CAS失败后yield让出cpu,操作系统执行t2,t2执行CAS失败yield让出cpu…),导致cpu还是在多个线程之间切换,不会使得获取了锁的线程执行代码的时间缩短,反而可能变长

sleep+自旋

这种方法问题在于:sleep时间不容易确定。如果获取锁的线程要执行3分钟,其他线程睡眠几秒钟效果不大;如果只需要执行0.5s,但是睡眠了10s,就会浪费获取锁的机会和时间。

park+自旋

获取不到锁的时候让线程释放cpu资源进行等待,当持有锁的线程释放锁的时候将等待的线程唤起。park()方法并不是由LockSupport类提供的,LockSupport仅仅对park()方法做了封装,park()方法是由unsafe类提供的。
ReentrantLock的底层就是通过park方法进行阻塞的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值