手写AQS锁解决秒杀超卖

业务场景

电商活动的秒杀场景,在并发过来的时候很容易出现库存扣超的情况,一个很简单的例子是,两个线程同时拿到了库存为1的数据,同时扣减库存,那么库存就会变为负的。

所以要解决这个问题还是要让请求串行,串行的最好方式就是加锁,今天我们要探讨的是AQS的锁实现。

AQS锁的三大核心
  • 自旋
  • lockSupport
  • CAS
自旋

获取锁失败的线程会通过自旋的方式重复获取锁,自旋也可以理解为死循环。

lockSupport

lockSupport解决的问题是线程自旋过程中浪费cpu资源的问题,他的原理是阻塞当前自旋线程,直到被唤醒后再次自旋获取锁。

CAS

CAS是获取锁的关键,他是属于一种无锁状态下的一个原子操作,更新时我们需要传入预期值和更新值,只有我们的预期值与当前的内存值相同时才允许改变。
在这里插入图片描述
CAS的问题

  1. ABA的问题

在这里插入图片描述
ABA的问题大概如上图所示

  1. T1期望值A,想要改成B成功
  2. T3期望值B,想要改成A
  3. T2和T1同样,只不过他阻塞住了,如果T2在T3完成后获取到cpu的时间片,将会将A重新改为B

解决ABA的问题的方案是增加一个版本号,对内存值维护一个版本号。

实现AQS锁

设计方案如图
在这里插入图片描述
依据这种设计至少维护三个相关变量

  • state // cas操作的变量
  • lockHolder // 持有锁的线程
  • waiters // 等待队列,将阻塞的线程放进队列
加锁代码
	private int state = 0;

    private Thread lockHolder;

    private ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<>();
	// 通过Unsafe进行cas操作
    private static final Unsafe unsafe = UnsafeInstance.getInstance();

    private static long stateOffset;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加锁
     */
    public void lock(){
        // 同步获取锁
        if(acquire()){
            return;
        }
        Thread current = Thread.currentThread();
        // 获取锁失败的 添加进队列里
        waiters.add(current);
        // 自旋加锁
        for(;;){
            if (current == waiters.peek() && acquire()){
                // 移除队列
                waiters.poll();
                return;
            }
            // 让出cpu的使用权
            LockSupport.park(current);
        }
    }

获取锁的实现
	/**
     * 获取锁
     * @return
     */
    private boolean acquire() {
        int state = getState();
        Thread current = Thread.currentThread();
        // 获取锁成功有两种情况 第一种是第一个获取到的队列里没有等待的  第二种是唤醒队列里等待的线程
        if ((waiters.size() == 0 || current == waiters.peek()) && state ==0){
            // 没有线程获取到锁
            if(compareAndSwapState(0,1)){
                // 同步修改成功 将线程持有者修改为当前线程
                setLockHolder(current);
                return true;
            }
        }
        return false;
    }
cas操作
 	/**
     * cas操作
     * @param expect
     * @param update
     * @return
     */
    public final boolean compareAndSwapState(int expect,int update){
      return  unsafe.compareAndSwapInt(this,stateOffset,expect,update);
    }
解锁代码
	/**
     * 解锁
     */
    public void unlock(){
        // 1.校验释放锁的线程是不是当前持有锁的线程
        if (Thread.currentThread() != lockHolder){
            throw new RuntimeException("threadHolder is not current thread");
        }
        // 2. 释放锁修改state
        if(getState() == 1 && compareAndSwapState(1,0)){
            // 将锁的持有线程置为空
            setLockHolder(null);
            // 2.唤醒队列里的第一个线程
            Thread first = waiters.peek();
            if (first != null){
                LockSupport.unpark(first);
            }
        }
    }
压测

为了模拟压测,写了一个秒杀的demo

@RestController
@RequestMapping("/shop")
public class ShoppingController {

    private volatile int trick = 5;
    private MyLock lock = new MyLock();
    private static final Logger logger = LoggerFactory.getLogger(ShoppingController.class);

    @RequestMapping("/go")
    public void shopping(){
//        lock.lock();
        if (trick >0){
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            trick = trick -1;
            logger.info("余票 trick = {}",trick);
        }else {
            logger.info("秒杀失败,余票 trick = {}",trick);
        }
//        lock.unlock();
    }
}

无锁压测,200个并发压测
在这里插入图片描述
很明显出现了超卖。下面进行有锁200并发压测
在这里插入图片描述
很明显没有出现并发。

备注:并发测试使用的ab,先是用的postman,发现请求是串行的。

扫码关注公众号

在这里插入图片描述

### 实现自定义AQS同步器 为了创建一个基于`AbstractQueuedSynchronizer`(简称AQS)的自定义同步组件,在设计之初应当明确该同步机制的核心逻辑——即如何管理共享资源的状态(state),以及在此基础上构建或其他同步工具的具体行为模式[^2]。 #### 定义同步状态的操作方法 在继承AQS类之后,开发者主要需关注两个方面的方法实现: - **独占式获取与释放**:通过覆写`tryAcquire(int arg)`和`tryRelease(int arg)`函数来规定尝试占有或者放弃独占权限的行为准则。这通常涉及到对内部state变量的安全修改,确保这些变更能够在线程间保持一致性和原子性。 - **共享式获取与释放**:如果希望支持多个线程同时访问,则要覆盖`tryAcquireShared(int arg)` 和 `tryReleaseShared(int arg)` 方法,它们决定了是否允许多个请求者共同持有资源。 下面是一个简单的例子展示了一个只允许单一线程执行的关键区域控制结构OnlySyncByAQS: ```java import java.util.concurrent.locks.AbstractQueuedSynchronizer; public class OnlySyncByAQS { private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); } private static final class Sync extends AbstractQueuedSynchronizer { protected boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { // 使用CAS操作保证设置状态时的原子性 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); clearExclusiveOwnerThread(); setState(0); return true; } protected boolean isHeldExclusively() { return getState() == 1 && getExclusiveOwnerThread() != null; } Condition newCondition() { return new ConditionObject(); } } } ``` 上述代码片段展示了如何利用AQS框架快速搭建起基本功能完整的互斥定装置,并且可以通过调整`tryAcquire()`中的条件表达式来自定义更复杂的业务规则[^3]。 此外,值得注意的是,这里的`setState()`、`compareAndSetState()`等都是由父类提供的用于安全地改变同步状态的基础API;而`setExclusiveOwnerThread()`则是用来记录当前拥有独占权的线程对象的信息[^4]。 最后,关于`state`字段的理解至关重要,它作为整个同步过程的灵魂所在,不仅代表着资源的实际可用情况,还可能承载着诸如重入次数之类的额外含义。例如,在可重入的设计里,每当同一线程再次申请已持有的时,就会相应增加此计数器的值,直至最终全部解为止[^5]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值