栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

手写CAS自旋锁,并设置阻塞线程等待策略

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

手写CAS自旋锁,并设置阻塞线程等待策略

理解自旋锁

自旋锁(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 - 美团技术团队​​​​​​

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/685286.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号