大家好,我是Coder哥,在技能一日千里的今日,真实应该花费时刻学习的是那些不变的编程思维,今日咱们来接着上一篇文章来聊一下锁思维,咱们上一篇”读写锁“详细的剖析了读写锁处理线程饥饿的思维。那么今日咱们再来聊另一个思维: 自旋。
说到自旋锁,许多Java开发者第一时刻可能会想到CAS,因为CAS 其实是面试中的常客,并且在底层原理和咱们实际运用中也是比较常出现的一种思维。比方: ConcurrentHashMap,原子类,Mysql达观锁的完成等,咱们接下来会详细的探讨。为了能阐明问题,咱们从以下几个问题由浅入深的来逐个回答一下,在看之前自己也多考虑考虑为什么。
- 自旋和CAS别离是什么以及他俩的联系是什么?
- 什么时分会用到自旋锁及CAS呢?
- 自旋及CAS的长处及缺陷是什么?
自旋和CAS别离是什么以及他俩的联系是什么?
咱们简略的来解释一下这两个都是啥意思以及他们之间的联系
自旋
首要,咱们来看一下什么叫自旋?顾名思义,自旋能够了解为“自我旋转”,放到程序中便是”自我循环”,比方while循环或者for循环。结合着锁来了解的话便是,先获取一次锁,假如获取不到锁,会不断的循环获取,直到获取到。不像普通的锁那样,假如获取不到锁就进入堵塞状况。
自旋和非自旋的获取锁的流程
咱们来看一下自旋锁和非自旋锁的获取锁的过程
能够看到,自旋锁没获取到锁并不会开释CPU时刻片,而是经过自旋等候锁的开释,也便是说,它会一向测验获取锁,假如获取失利就再次测验,直到成功停止。
CAS
CAS 是什么,它的英文全称是 Compare-And-Swap,中文叫做“比较并交流”,它是一种思维、一种算法。
CAS算法有3个根本操作数:
- 内存地址V
- 旧的预期值A
- 要修正的新值B
在并发场景下,各个代码的履行次序不能确定,为了保证并发安全,咱们能够运用普通的互斥锁,比方Java的 synchronized, ReentrantLock等。而CAS的特点是防止运用互斥锁,当多个线程并发运用CAS更新同一个变量时,只要一个能够操作成功,其他都会失利。并且用CAS更新失利的线程并不会堵塞,会快速失利并回来一个失利的状况,答应你再次测验。
自旋锁和CAS的联系是啥
其实他们是两个不同的概念
自旋是一种锁优化的机制,在锁优化中『自旋锁』指线程空转重试获取锁,防止线程上下文切换带来的开支。
CAS是一种达观锁机制,cas是经过比较并交流,失利的时分能够直接回来false不必自旋的获取。只是一般运用场景下,cas都会带有重试机制(while或者for完成空转,不断测验获取)。
假如非要加个联系的话 自旋锁 = 循环+CAS。
什么时分会用到自旋锁及CAS呢?
在一般并发场景下,加普通锁根本能够处理一切的安全问题,可是为什么还分各种锁,悲观/达观,公正/非公正,读写锁呢?《可查看Java并发-锁思维 系列文章》因为在许多场景下,咱们同步的代码块内容并不多,需求的履行时刻也很短,假如咱们每次都去切换线程状况,势必会带来上下文切换等开支,假如不切换线程而是让它自旋的测验获取锁,等候其他线程开释锁,就能够防止上下文切换等开支,提就了功率。
所以Doug Lea 大神在 JUC 包中大量运用了 CAS 算法,该算法既能保证安全性,又不需求运用互斥锁,能大大提高锁的功能。下面咱们能够看一下:
并发容器:ConcurrentHashMap
先来看一下并发容器 ConcurrentHashMap 的 putVal 办法的代码,如下所示:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
...
}
上面代码中有一个醒目的办法,它便是 casTabAt(...)
,这个办法名就带有 “CAS”,能够猜测它一定是和 CAS 密不可分了,下面给出 casTabAt 办法的代码完成:
private static final sun.misc.Unsafe U ;
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
该办法里面只要一行代码,即调用变量 U 的 compareAndSwapObject 的办法,能够看出,U 是 Unsafe 类型的,Unsafe 类下的 compareAndSwapInt、compareAndSwapLong、compareAndSwapObject 等是和 CAS 密切相关的 native 层的办法,底层原理便是经过 CPU 对 CAS 指令的支撑完成的。
上面的 casTabAt(...)
办法,不只被用在了 ConcurrentHashMap 的 putVal 办法中,还被用在了 computeIfAbsent、merge、compute、transfer 等重要的办法中,所以 ConcurrentHashMap 关于 CAS 的运用是比较广泛的。
这个是CAS在并发容器中的事例,除了这个其他的并发容器中也有,比方说:非堵塞并发行列 ConcurrentLinkedQueue 的 offer 办法里也有 CAS 的身影,这儿就不贴代码了,有兴趣的能够自行查阅。
下面咱们来看一下自旋在原子类中的运用:
AtomicLong 的完成
在 Java 1.5 版别及以上的并发包 java.util.concurrent 中,里面的原子类根本都是自旋锁的完成。
比方咱们来看一个 AtomicLong 的完成,里面有一个 getAndIncrement 办法,源码如下:
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
能够看到它调用了一个 unsafe.getAndAddLong,所以咱们再来看这个办法:
public final long getAndAddLong (Object var1,long var2, long var4){
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
咱们看到上述办法中有对 var6 的赋值,调用了 unsafe 的 getLongVolatile(var1, var2) 办法,这是一个 native 办法,作用是获取变量 var1 中偏移量 var2 处的值。这儿传入 var1 的是 AtomicLong 目标的引用,而 var2 便是 AtomicLong 里面所存储的数值(也便是 value)的偏移量 valueOffset,所以此时得到的 var6 实际上代表当时时刻下的原子类中存储的数值。
接下来重点来了,咱们看到有一个 compareAndSwapLong 办法,这儿会传入多个参数,别离是 var1、var2、 var6、var6 + var4,其实它们代表 object、offset、expectedValue 和 newValue。
- 第一个参数 object 便是将要修正的目标,传入的是 this,也便是 AtomicLong 这个目标自身;
- 第二个参数 offset,也便是偏移量,借助它就能够获取到 AtomicLong自身 value 的数值;
- 第三个参数 expectedValue,代表“期望值”,传入的是方才获取到的 var6;
- 最后一个参数 newValue 是期望修正为的新值 ,等于之前取到的数值 var6 再加上 var4,而 var4 便是咱们之前所传入的 delta,delta 便是咱们期望原子类所改变的数值,比方能够传入 +1,也能够传入 -1。
所以 compareAndSwapLong 办法的作用便是,判别假如现在AtomicLong里 value 的值和之前获取到的 var6 持平的话,也便是没有被其他线程修正正,那么就以为是安全的,就把核算出来的 var5 + var4 给更新上去,那么就会退出循环。
可是假如var6的值被其他线程修正了,那么这儿的 ”期望值“ 就不相同了,那么 this.compareAndSwapLong(var1, var2, var6, var6 + var4)
会回来 false, 就会持续履行循环体再次获取var6的值,这儿是个死循环直到获取到最新的没被其他线程修正正的”期望值“ 停止,所以这儿的compareAndSwapLong 其实便是CAS算法的完成,而do-while循环正是自旋的思维。
除了这些,自旋和CAS在咱们调用数据库的事务场景中也有运用:
数据库
在咱们的数据库中,也存在对达观锁(【锁思维】功能提高之道-悲观锁和达观锁原理及场景剖析)和 CAS 思维的运用。比方咱们在更新数据的时分,能够利用 version 字段在数据库中完成达观锁和 CAS 操作,而在获取和修正数据时都不需求加悲观锁。
详细思路如下:
在数据获取和核算完成后,咱们会查看当时版别号与之前获取数据时的版别号是否相同。假如相同,表示在核算期间数据没有被更新,能够直接更新本次数据。假如版别号不同,阐明在核算期间有其他线程修正了数据,咱们能够选择从头获取数据、从头核算,然后再次测验更新数据。
假定数据获取时的版别号为1,相应的SQL语句如下:
UPDATE student SET name = '小王', version = 2 WHERE id = 10 AND version = 1
经过这种方法,咱们能够运用CAS(比较并交流)的思维来完成本次的更新操作。首要,它会比较版别号是否与最初获取的1相同,只要相同才会进行name字段的修正,并将版别号加一。
如事务有需求,在事务层的处理逻辑能够依据 UPDATE的回来值,循环几次,来自旋的获取,保证能够更新成功。
咱们能够看出, CAS 和自旋思维 在并发容器、数据库、原子类及自旋的事务场景中都有许多和 CAS 相关的运用及代码,所以 CAS和自旋都 有着广泛的运用场景。
自旋及CAS的长处及缺陷是什么?
从上面的介绍能够看出,自旋锁拿不到锁的时分会不断地测验。那么,自旋锁这样不断测验的好处是什么呢?
自旋锁及CAS的好处
首要,堵塞和唤醒线程的开支很高。假如同步代码块的内容不复杂,线程切换的开支可能比实际事务代码的履行开支还要大。这儿能够参考公正和非公正锁文章
在许多情况下,同步代码块的内容并不多,履行时刻很短。假如咱们只是为了这点时刻就切换线程状况,实际上不如让线程保持原状况,自旋地测验获取锁,等候其他线程开释锁。有时分,咱们只需求稍等一下,就能防止上下文切换等开支,提高功率。
总结自旋锁的好处,便是经过循环不断地测验获取锁,让线程始终处于Runnable状况,节省了线程状况切换的开支。
自旋锁及CAS的缺陷
自旋时刻过长
自旋锁可能会出现自旋时刻过长。
因为单次CAS操作不一定能成功,一般需求结合循环来完成。有时乃至需求运用死循环,不断重试,直到线程竞赛不激烈时才干成功修正。
可是,假如咱们的运用场景自身便是高并发的,可能导致CAS一向无法成功。这样循环的时刻会越来越长。一起,在此期间,CPU资源也会持续消耗,对功能产生很大影响。因而,咱们需求依据实际情况来选择是否运用CAS。在超高并发场景下,一般CAS的功率并不高。
规模不能灵敏控制
CAS 不能灵敏控制线程安全的规模。
一般情况下,咱们履行CAS操作时针对的是单个同享变量,这个变量能够是Integer、Long、目标等类型。可是咱们不能一起对多个同享变量进行CAS操作,因为这些变量之间是独立的,简略地将原子操作组合在一起并不具备原子性。因而,假如咱们想要对多个目标一起进行CAS操作并保证线程安全,会比较困难。
有一种处理方案是利用一个新的类来整合这组同享变量,新类中的多个成员变量便是之前的多个同享变量。然后,运用atomic包中的AtomicReference来对这个新目标进行整体CAS操作,这样就能够保证线程安全。
相比之下,假如咱们运用其他的线程安全技能,调整线程安全的规模可能会更简单。例如,运用synchronized关键字时,假如想要将更多的代码加锁,只需求将更多的代码放入同步代码块中即可。
ABA 问题
CAS的最大缺陷是ABA问题。
CAS的判别规范是当时值和预期值是否共同,假如共同,则以为在此期间该值没有产生改变,这在大多数情况下是没有问题的。
可是,在某些事务场景下,咱们期望切当知道从上一次观察到该值到现在,该值是否产生过改变。例如,假定该值从A变为B,然后又从B变回A,咱们不只以为它产生了改变,并且以为它产生了两次改变。
在这种情况下,运用CAS无法观察到这两次改变,因为只是判别当时值和预期值是否持平是不够的。CAS查看的是值是否产生改变,而不是值是否和本来值不相同。假如变量的值从旧值A变为新值B再变回旧值A,因为最初的值A和现在的值A是持平的,CAS会以为变量的值在此期间没有产生改变。因而,CAS无法检测出值是否在此期间被修正正,它只能查看当时值和最初值是否相同。
举个比如,假定第一个线程获取的初始值是100,然后进行核算过程中,第二个线程将初始值改为200,然后又将其改回100。当第一个线程核算完毕并履行CAS时,它会比较当时值是否等于最初获取的初始值100,发现它的确等于100,因而线程一会以为在此期间值没有被修正正,并将其改为核算出的新值。可是,在此过程中,其他线程已经修正了该值,这就导致了ABA问题的产生。
那么如何处理这个问题呢?一个处理方案是增加一个版别号。
咱们能够在变量值之外增加一个版别号,这样值的改变途径从ABA变为1A→2B→3A。经过比较版别号来判别值是否产生过改变比直接比较两个值是否持平更可靠,因而经过这种方法能够处理ABA问题。
在atomic包中,提供了AtomicStampedReference类,它专门用于处理ABA问题。它维护了一个类似<Object, int>的数据结构,其间的int便是用于计数的版别号。AtomicStampedReference能够对这个目标和版别号一起进行原子更新,从而处理ABA问题。因为咱们判别值是否被修正不再以值是否产生改变为规范,而是以版别号是否改变为规范,即使值相同,它们的版别号也是不同的。
以上便是对CAS的最严峻的一个缺陷——ABA问题的介绍。
总结
本篇文章首要围绕自旋和CAS来打开介绍,首要介绍了他们俩的区别,关于锁来讲自旋的思维给程序带来的功能提高,以及在Java中CAS和自旋的适用场景等,这儿就不再赘述了。
感谢大家能看到这儿,写文章不易,假如觉得有用记得收藏点赞,感谢!
《锁思维》 系列文章:
【锁思维】锁的7大分类及特点 – 了解并发编程中的锁机制
【锁思维】功能提高之道-悲观锁和达观锁原理及场景剖析
【锁思维】为什么各种语言中锁完成的默许战略都是非公正的?
【锁思维】高并发下 读写锁是经过什么战略来防止写线程饥饿的?