美文网首页javacode
动手实现一个同步器(AQS)

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

作者: 九点半的马拉 | 来源:发表于2020-02-26 23:28 被阅读0次

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

    同步器的加锁

    我将自己实现的同步器成为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();
            }
        }
    }
    

    测试结果:

    测试结果.png

    相关文章

      网友评论

        本文标题:动手实现一个同步器(AQS)

        本文链接:https://www.haomeiwen.com/subject/xkqnchtx.html