动手实现一个同步器(AQS)

Posted 房东的小黑黑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动手实现一个同步器(AQS)相关的知识,希望对你有一定的参考价值。

在多线程情景下,如果不会某一共享变量采取一些同步机制,很可能发生数据不安全现象,比如购买车票时,当多个人购买时,不加锁就会产生多人买同一张票的现象,显然这是不可取的。所以要有一种同步机制,在某一时刻只能有一个线程处理该共享变量。

同步器的加锁

我将自己实现的同步器成为RoadAQS.
主要变量如下:

//当前锁的状态,1表示加锁,0表示未加锁
private volatile int state = 0;
private final static Unsafe unsafe = UnsafeInstance.reflectUnsafe();
//state在内存中的偏移量
private final static long stateOffset;
//当前持有锁的线程
private Thread lockHoder;
//是一个线程安全的队列,记录等待获取锁的线程
private ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<>();

static {
        try {
            stateOffset = unsafe.objectFieldOffset(RoadAQS.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }

整体思想:
刚一开始初始化时会利用反射获取一个Unsafe魔法类,然后获取变量state在内存中的偏移量,为后续的CAS操作做准备。然后开始尝试获取锁,当等待队列为空或者当前线程等于等待队列的第一个线程,然后CAS更新状态为1成功,说明获得锁成功,并将同步器的拥有者设置为当前线程。如果加锁失败,就将该线程放入到等待队列中,然后开始无限for循环。
进入循环背内部,再尝试一次获取锁,仍然失败后,开始调用LockSupport.park()将该线程进行阻塞,与Object.wait一个最大的区别就是park()、unpark()能够指定具体的线程进行唤醒,而object.notify只能随机唤醒一个。
阻塞后当其他线程执行完退出后,会调用LockSupport.unpark(t)对等待队列中的第一个线程进行唤醒,唤醒后会继续执行for循环内部的代码,再尝试获得锁。获得锁后,从等待队列中取出,并将同步器的拥有者改为该线程。

public void lock() {
        if(acquire()){
            return;
        }
        Thread current = Thread.currentThread();
        waiters.add(current);
        for(;;) {
            if(current == waiters.peek() && acquire()) {
                waiters.poll();
                return;
            }
            LockSupport.park();
        }
    }
public boolean acquire() {
        Thread t = Thread.currentThread();
        if ((waiters.size() == 0 || t == waiters.peek()) && compareAndSwapInt(0, 1)) {
            setLockHoder(t);
            return true;
        }
        return false;
    }

同步器的解锁

获取当前的锁状态,并尝试更新为0,成功后将同步器的拥有者设为null,然后获取等待队列的第一个队列,将该队列进行唤醒。

public void unlock() {
        if (Thread.currentThread() != getLockHolder()) {
            throw new RuntimeException("lockHolder is not current Thread");
        }
        int state = getState();
        if (compareAndSwapInt(state, 0)) {
            setLockHoder(null);
            Thread t = waiters.peek();
            if (t != null) {
                LockSupport.unpark(t);
            }
        }
    }

测试用例

public class RoadAQSTest {
    public static void main(String[] args) {
        Goods goods = new Goods();
        for(int i=0; i<100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    goods.reduceCount();
                }
            }, "Thread-" + i + "------").start();
        }
    }

    private static class Goods{
        private int count = 10;
        private RoadAQS lock = new RoadAQS();
        public void reduceCount() {

            lock.lock();

            if (count > 0) {
                System.out.println("线程" + lock.getLockHolder() + " 获取第 " + count + "件商品");
                count--;
            } else {
                System.out.println("商品已卖完!");
            }
            lock.unlock();
        }
    }
}

测试结果:
测试结果

以上是关于动手实现一个同步器(AQS)的主要内容,如果未能解决你的问题,请参考以下文章

AQS同步队列器之一:介绍以及简单使用

JUC回顾之-AQS同步器的实现原理

源码级深挖AQS队列同步器

Java并发框架——AQS之怎样使用AQS构建同步器

AQS(队列同步器)

从ReentrantLock加锁解锁角度分析AQS