实现一个简单的抽象队列同步器---理解AQS底层原理

AQS是 AbstractQueuedSynchronizer ,译为抽象的队列同步器。它主要为了保证我们高并发场景下的一个线程安全。这里我们通过自己实现一个简单版本的AQS来理解他的核心原理。

一、同步加锁思路

首先保证加锁的话,任意时刻只能是有一个线程加锁成功,也就是保证它的原子性。这里就提供一个 state 这样一个字段,来记录当前加锁的一个状态,也就是次数。我们要做的就是保证任意时刻,只有一个线程拿到这个锁,当线程拿到这个锁之后,就对这个 state 值进行一个修改,把他改成1。这里为了保证只有一个线程拿到这个值,可以使用 CAS 比较交换的原子算法(Atomic原子类就是通过这个算法来实现)。

使用 CAS 的话就要用到 Unsafe 这个类,这个类可以绕过虚拟机直接去操作底层的内存,这个类中的 API 都是 nativa 方法,这里我们要调用的就是这三个 native 方法。如图:

在这里插入图片描述
还需要有 lockholder 这个字段来存储当前持有锁的线程,以方便我们对线程进行一个操作代码如下:

/**
 * 记录当前加锁的状态,0表示无线程持有,1表示有一个线程持有锁。
 */
private volatile int state = 0;
/**
 * 当前持有锁的线程
 */
private Thread lockHolder;

public int getState() {
    return state;
}

public void setLockHolder(Thread lockHolder) {
    this.lockHolder = lockHolder;
}

二、使用Unsafe类

要想实现同步操作,肯定就要了解原子操作,所谓原子操作就是执行过程中无法被打断这样一种操作,例如赋值操作。

要实现这样的原子操作,就需要去调用上面说到 Unsafe 这个类中的方法,但是在 Java 中是不建议去使用这个类的。顾名思义,这个类是不安全的。如果我们直接去 new 新建的话,Java 中会直接报错,他这里内部给定了一个 getUnsafe() 方法,根据双亲委派机制,限定你这个类必须通过引导类加载器(bootstrap classloader,又称启动类加载器)加载出来,才可以去使用。否则就会抛出一个安全异常。源码如下:

在这里插入图片描述
由于上述原因,我们就只能通过反射的方法来获取到这个 Unsafe 类,这里创建一个 UnsafeInstance 类,代码如下:

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeIntance {
    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            //设置为可访问
            field.setAccessible(true);
            return (Unsafe)field.get(null);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }
}

三、compareAndSwapInt()方法使用

我们使用 Unsafe 类主要就是需要通过他的 CAS 的方法来对我们的 state 字段进行一个修改,但是这个方法一共有四个参数,源代码如下:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

这四个参数类型分别为 Object ,long, int ,int 。

  1. Object var1:第一个 Object 类型参数,代表的就是当前的一个类对象,也就是传入我要修改字段的类名。
  2. long var2:第二个 long 类型参数表示的是我要修改的字段在这个对象结构中的一个偏移量。对象在内存中的结构主要是对象头、实例数据区和对齐填充。这里可以参照《Java对象的结构组成》这篇文章。我们要找到字段在类中的一个位置,就需要获取到这个字段在数据区中的一个偏移量(从对象存储的起始地址开始)。通过这个偏移量,就能获取到这个字段了。由于CAS需要原始数据值、预期值还有修改值。通过类名和偏移量,就可以找到原始数据值了。
  3. int var4:CAS 的预期值 expect。
  4. int var5:CAS 的修改值 update。

该部分代码如下:

 public final boolean compareAndSetState(int expect, int updata) {
     return UNSAFE.compareAndSwapInt(this, STATEOFFSET, expect, updata);
 }

 /**
  *  通过反射获取一个 Unsafe 实例对象
  *  调用刚才 UnsafeIntance 类的 reflectGetUnsafe()方法
  */
 private static final Unsafe UNSAFE = aqs.UnsafeIntance.reflectGetUnsafe();

 /**
  * state字段偏移量
  */
 private static final long STATEOFFSET;

 static {
     try {
         //获取偏移量
         STATEOFFSET = UNSAFE.objectFieldOffset(Aqs.class.getDeclaredField("state"));
     } catch (Exception e) {
         throw new Error();
     }
 }

四、加锁

首先进行加锁操作,需要先取到当前线程,然后再通过 getState() 方法来获取到当前这个类对象中的 state 字段,然后对这个 state 进行一个判断,如果为 0,就说明当前这个锁还没有被持有。这里的思路其实就是多个线程为了获取锁,去争抢着通过 CAS 修改 state 这个字段,修改为1,这时 compareAndSwapInt() 这个方法会返回true,因为你是修改成功了,然后就把 lockHolder 设置为该线程,并且让他持有锁。此时其他线程再看到 state 已经为1了,就说明这个值已经被修改了,也就是说他们修改失败,会返回一个 false 。

上面说到的是线程修改成功,并且加锁成功,但此时其他线程加锁失败,我们不能让他从方法中出来,因为一旦出来了,就会和加锁的线程一样执行后面的逻辑代码。为了避免让加锁失败的线程跳出方法,这里就通过一个循环,让他在循环里面不断地自旋。但是也不能让他们一直处在死循环的状态,这样会浪费 CPU 资源。这里就可以用到 Unsafe 类另外提供的方法,park() 和 unpark()方法,如图:
在这里插入图片描述
这两个方法也是 native 本地方法,这两个方法就是用来阻塞/唤醒线程的。当然这里我们不用去直接调用 Unsafe 类中的这两个方法,可以通过 LockSupport 这个类来实现阻塞/唤醒线程,这个类里面提供了 park() 和 unpark() 的API,在内部就包装了 Unsafe 类中的 native 方法。如图:
在这里插入图片描述
在这里插入图片描述

所以说,为了避免获取锁失败而继续占用 CPU 资源,就可以调用 LockSupport.park() 方法来对获取锁失败线程进行一个阻塞。但这里还有一个问题,我们不能只把线程阻塞住就不管了,在之后也需要其他线程加锁,所以我们使用 ConcurrentLinkedQueue 这个队列来保存加锁失败的线程。代码如下:

/**
 * 线程安全的队列---基于CAS算法
 */
private static ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();

至于为什么不是用 ArrayBlockingQueue 和 LinkedBlockingQueue 是因为这两个阻塞式队列就是基于AQS来实现的,我们当前就是要实现一个简单的AQS,所以说暂时放弃这两个队列。

由于是公平锁,所以说这里需要进行一个判断,看看在你这个线程之前还有没有线程排队了,也就是队列中还有没有线程,或者说你当前这个线程就是出于队列头部,你的优先级最高。如果队列是空的,或者你就是队列的队首元素,就可以让你进行加锁操作。

整个加锁过程代码如下:

/**
 * 线程安全的队列---基于CAS算法
 */
private static ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();

/**
 * 尝试获取锁
 */
public boolean aquire() {
    Thread current = Thread.currentThread();
    //当前加锁的状态
    int c = getState();
    if (c == 0) {
        //当前同步器还没有被持有
        //canUse用来确定当前这个尝试加锁的线程是不是满足公平锁条件
        boolean canUse = waiters.isEmpty() || current == waiters.peek();
        if (canUse && compareAndSetState(0, 1)) {
            //修改成功的线程,设置为持有者
            setLockHolder(current);
            return true;
        }
    }
    return false;
}

/**
 * 加锁后的处理
 */
public void lock() {
    //加锁成功
    if (aquire()) {
        return;
    }
    //没有加锁成功
    Thread current = Thread.currentThread();
    waiters.add(current);
    //自旋:死循环确保没有获取到锁的线程不再 执行后续代码/占有CPU
    for (;;){
        if (aquire()) {
            //唤醒队头线程的话,就需要从队列中移除该线程,让后面的线程排到队首
            waiters.poll();
            return;
        }
        LockSupport.park();//阻塞--释放CPU使用权,被刷入到运行时状态段
    }
}

五、释放锁

首先需要判断一下当前线程,你到底是不是当前的锁持有者 lockHolder 。如果你是当前的锁持有者,就再去调用 compareAndSwapInt() 方法,把 state 这个字段再进行一个修改,把1修改为0 。也就表明是一个初始状态—无锁状态,修改之后我就把当前这个锁持有者修改为 null 。

但释放这个锁之后,我还需要去唤醒在队列中的其他线程,此时就可以去除队首元素 first ,如果你不是 null 的话,就调用 LockSupport.unpark() 方法,唤醒 first 。这里我们不是用 wait() 是因为我需要指定唤醒某一个线程,而不是通过 notify() 和 notifyAll() 去随机性的唤醒线程。

代码如下:

/**
 * 释放锁
 */
public void unlock() {
    if (lockHolder != Thread.currentThread()) {
        //抛出异常
        throw new RuntimeException("Lockholder is not current thread");
    }
    int state = getState();
    if (compareAndSetState(state, 0)) {
        setLockHolder(null);
        //拿到队首元素这个线程的引用。
        Thread first = waiters.peek();
        if (first != null) {
            //就需要唤醒队首线程
            LockSupport.unpark(first);
        }
    }
}

这里我们只是简单实现了一个 队列同步器,真正AQS的实现还是有其他附加的功能,不过可以通过自己实现来理解一下AQS的核心内部原理。

GitHub源码:https://github.com/zhangdididi/MyAbstractQueuedSynchronizer

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值