自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting,也就是线程空转。自旋锁优点是所有线程都是“运行”状态,不需要唤醒线程,速度较快;不足则是线程的空转导致无谓的资源损耗。针对这类问题也有相关的等待策略进行优化,应对各种场景,合理利用资源。
理解CAS概念一、什么是CAS
CAS,compare and swap的缩写,中文翻译成比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。
二、CAS的目的
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
接下来分三部分,来理解自旋锁的实现方式
1.不可重入自旋锁
不可重入锁比较好理解:当一个线程获得锁后执行任务,那么所有的线程包括自己都被阻塞在外
用CAS实现一个不可重入自旋锁
package com.xiaoze.provider.service.impl;
import java.util.concurrent.atomic.AtomicBoolean;
public class NonReentrantLock {
private AtomicBoolean flag = new AtomicBoolean(false);
public void lock(){
while (flag.getAndSet(true)){
}
}
public void unlock(){
flag.set(false);
}
}
1)使用了一个线程安全的boolean对象用来实现不可重入锁,该对象所提供的取值、赋值、比较等方法都是原子方法,利用此基础实现不可重入锁
2)先看lock方法的实现。假设两个线程A,B。A首先进入lock方法,在while条件中结果为false,而将flag值修改为ture,不进入自旋;此时第二个线程B进入,在while条件中结果为true,并将flag值修改为true,进入自旋。线程B就会一直在while循环中空转,达到等待的效果。不可重入的特性体现在所有的线程在A线程获得锁的时候都会进入到阻塞自旋。
3)unlock方法解除锁定,所有在阻塞中的线程都将有机会获得锁并执行任务
2.可重入自旋锁
可重入锁与不可重入锁的区别是允许获得锁的线程再次进入,其他线程则进入自旋等待
下面是不可重入锁的CAS实现
package com.xiaoze.provider.service.impl;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class SimpleLock {
private AtomicReference threadAtomicReference=new AtomicReference<>();
private AtomicBoolean flag = new AtomicBoolean(true);
public void lock(){
Thread current = Thread.currentThread();
if(flag.getAndSet(false)){
threadAtomicReference.set(current);
}
for(int count=1;!(threadAtomicReference.get()==current);count=doWaitFor(count)){
;
}
}
public void unlock(){
Thread current = Thread.currentThread();
if(current==threadAtomicReference.get()){
flag.set(false);
threadAtomicReference.set(null);
}
}
public int doWaitFor(int count){
Thread.yield();
return count;
}
}
1)lock分为两步,第一步为获取锁的过程,A线程率先进入lock,if判断结果为ture,并将flag赋值为false,若此时线程B紧接着进入lock,if判断结果为false,将flag赋值为false,将不能执行if判断中的语句,实现锁定A线程的目的;第二步是一个for循环自旋,当if判断执行完后,在for循环条件判断中比较是否为A线程,是则跳出循环,不是则将进入自旋。
可以发现lock并没有加锁,也不是原子操作,如何保证线程安全?
原理就是保证标记位的取值,比较,赋值过程具备原子性即可。在第二步中,其实线程A,B是不分先后的,阻塞与否只是根据条件判断,所以只需要保证第一步的线程安全即可。
2)先进行状态分析,当A线程unlock之前,A线程之外的所有线程(线程B,线程C)都在for循环自旋中。
因此先修改flag再修改threadAtomicReference,如果先将threadAtomicReference置空,那么B线程可能跳出自旋并执行任务,而B线程重入lock时,此时flag尚未置为true,将导致threadAtomicReference赋值失败,导致lock锁不能正确执行。
3.自旋等待方案以及Distruptor介绍
在不可重入锁中,我加入了一个等待策略doWaitFor(),该方法效果是当前线程在自旋一次后进入等待策略,进行一次Thread.yield(),该方法意思是线程从运行状态转为就绪状态,这样可释放cpu资源,但增加了线程的不公平性。
除此之外,还可以通过等待策略,降低自旋带来的资源损耗。
public void lock(){
Thread current = Thread.currentThread();
if(flag.getAndSet(false)){
threadAtomicReference.set(current);
}
for(int count=200;!(threadAtomicReference.get()==current);count=doWaitFor(count){
;
}
}
public int doWaitFor(int count){
if(count>100){
count--;
}else if (count>0){
count--;
Thread.yield();
}else{
LockSupport.parkNanos(1L);
}
return count;
}
上述代码中,将count初始值设置为200,自旋一次进入等待策略,当count在200和100之间时不设置策略,count自减,相当于自旋100次;当count在100和0之间,自旋一次后释放资源,count自减;当count为0时,自旋一次线程将睡眠1纳秒,实现降低自旋速度的目的,降低资源损耗。
等待策略方案来自于Distruptor,一个内存队列框架。消费者消费时等待策略中其中一种就是我所写的等待策略,在实际开发可以根据场景合理的设置等待策略。可以了解一下
高性能队列——Disruptor - 美团技术团队



