手写ReentrantLock-AQS实现原理分析

是什么

  作为Java开发人员,对于AQS(AbstractQueuedSynchronizer)抽象队列同步器都不会陌生,无论是面试别人,或者别别人面试的过程中,多多少少都会提到它。在J.U.C包下很多类的底层都是采用它来保证的多线程间同步的,本文就来讲讲问及度比较高的ReentrantLock的实现原理,主要是说下AQS的实现原理。


为什么

  有些人会说ReentrantLock代码在哪里摆着,自己去看看就明白怎么回事儿了,在此,我说下我写这篇文章的目的:1.用简单代码阐述AQS的原理,谁让源码那么多行呢。2.划重点,大家时间都比较宝贵,没太多时间扣那么细,先把重点划出来,日后真遇到问题了,有个突破点也好。


怎么做

  下面我就将逐步去分析实现的原理,这里分为三个类,测试类[MyLockTest],主要目的是模拟多线程来竞争同一资源,代码如下:

package com.springannotation.lock;


public class MyLockTest {

    /**
     * 自定义的锁对象
     */
    static MyLock myLock = new MyLock();

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread("线程" + i) {
                @Override
                public void run() {
                    // 获取锁
                    myLock.lock();
                    try {
                        // 线程doSomething
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // 有异常,释放锁
                        myLock.unLock();
                        e.printStackTrace();
                    }
                    // 正常释放锁
                    myLock.unLock();
                }
            };
            thread.start();
        }
    }
}

Copy

  主要关注点就是获取锁myLock.lock()和释放锁myLock.unLock();

  第二个类是UnSafe的包装类,主要是就通过反射获取到Unsafe魔术类(可以绕过虚拟机,直接操作内存)的实例,这个类在J.U.C包下的很多类中都有用到,我这里也是使用它提供的compareAndSwapInt()方法来原子化操作MyLock中state属性值的,为何通过反射来获取这个实例,我这里就不细说了,有兴趣的去看看源码为何这么设计吧。代码如下:

package com.springannotation.lock;

import sun.misc.Unsafe;

import java.lang.reflect.Field;


public class UnSafeWrapper {

    /**
     * 通过反射创建UnSafe类实例,
     * 禁止通过Unsafe.getUnsafe()来得到UnSafe实例,会得到throw new SecurityException("Unsafe");
     * 除非使用BootstrapClassLoader类加载器来获取这个UnSafe实例
     * 参看 https://www.cnblogs.com/throwable/p/9139947.html
     *
     * @return
     */
    public static Unsafe getUnSageInstanceByReflect() {
        try {
            // 通过反射得到UnSafe中的theUnsafe属性
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            // 允许通过反射访问私有属性,不然无法使用这个私有属性
            f.setAccessible(true);
            // 返回UnSafe实例
            return (Unsafe) f.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

Copy

  上面的类也都有注释,接下来要介绍的就是手写的Lock类了,主要有几个属性值,这里先介绍下:

  • state:用来标识获取锁状态,整型,同时可以表示获取锁次数。
  • holdLockThread:当前持有锁的线程。
  • unSafe:得到魔法类UnSafe的实例。
  • stateOffset:state属性的偏移量:在类被加载后,得到state的在类对象中的偏移量位置,后面用CAS更新的时候,需要使用到。

  下面看一下具体代码,100多行,加了注释,花2分钟看完就明白了:

public class MyLock {

    /**
     * 用来标识获取锁状态,整型,同时可以表示获取锁次数
     */
    @Getter
    @Setter
    private volatile int state = 0;

    /**
     * 当前持有锁的线程
     */
    @Getter
    @Setter
    private Thread holdLockThread;

    /**
     * 得到魔法类UnSafe的实例
     */
    private static final Unsafe unSafe = UnSafeWrapper.getUnSageInstanceByReflect();

    /**
     * state属性的偏移量:在类被加载后,得到state的在类对象中的偏移量位置,后面用CAS更新的时候,需要使用到
     */
    private static long stateOffset = -1;

    /**
     * 排队队列:获取锁失败的线程被阻塞后,存在队列中,方便被重新唤醒,继续进行获取锁操作
     * 这里要考虑线程安全,所以要是用线程安全的队列(不要用基于AQS实现的那些线程安全队列:例如BlockingQueue、我们是要自己写AQS逻辑)
     * ConcurrentLinkedDeque是基于CAS方式保证的线程安全
     */
    private ConcurrentLinkedDeque<Thread> queue = new ConcurrentLinkedDeque<Thread>();

    static {
        try {
            // 得到MyLock类中state属性的内存偏移量地址
            stateOffset = unSafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取锁
     */
    public void lock() {
        if (tryAcquire()) {
            // 获取锁成功,直接返回原来线程
            return;
        }
        // 得到当前用来获取锁的线程
        Thread current = Thread.currentThread();
        // 获取锁失败,将该线程放入到队列中
        queue.add(current);
        // 原来线程不能返回(自旋),要阻塞住(利用LockSupport.park()方法,主要目的是让出CPU)
        for (; ; ) {
            // 如果队列中的第一个线程是当前线程,尝试去获取锁
            if (current == queue.peek() && tryAcquire()) {
                // 将自己从等待获取锁队列中弹出
                queue.poll();
                // 获取锁成功,退出等待
                return;
            }
            System.out.println("[" + current.getName() + "]:获取锁失败,阻塞等待前一个线程释放锁.");
            // 阻塞当前线程
            LockSupport.park(current);
        }
    }

    /**
     * 尝试去获取锁
     *
     * @return 获取锁成功true/获取锁失败false
     */
    public boolean tryAcquire() {
        // 得到当前用来获取锁的线程
        Thread current = Thread.currentThread();
        // 如果当前state还是0,未有人获取锁
        if (0 == getState()) {
            System.out.println("当前锁没有被占用,[" + current.getName() + "]尝试去获取锁");
            // 获取锁成功依靠CAS操作,将state由0加到1,加成功了,则表示获取到了锁。(这里仅演示获取锁,不演示重入锁问题)
            // 队列中已经有排队的且队列第一个元素是当前线程
            if ((queue.size() > 0 || queue.peek() == current) && this.compareAndSwapState(0, 1)) {
                // 获取锁成功后,将holdLockThread设置为当前的线程
                setHoldLockThread(current);
                System.out.println(">>> [" + current.getName() + "]:获取锁成功");
                // 直接返回
                return true;
            }
        }
        return false;
    }

    /**
     * 释放锁
     */
    public void unLock() {
        // 判断当前线程是不是获取到锁的那个线程
        if (Thread.currentThread() != holdLockThread) {
            // 如果获取锁的线程不是当前线程,抛错回去
            throw new RuntimeException("你不能释放这个锁!");
        }
        // 如果当前线程是获取到锁的线程,进行锁释放
        // 将state置位
        if (compareAndSwapState(getState(), 0)) {
            // state置位成功后,将holdLockThread清空
            setHoldLockThread(null);
            // 释放锁完毕,判断下队列汇中是否有等待的线程,取出来,唤醒它
            Thread head = queue.peek();
            if (null != head) {
                // 唤醒队列中等待获取锁的线程,尝试去获取锁
                LockSupport.unpark(head);
            }
            System.out.println("<<< [" + Thread.currentThread().getName() + "]:释放锁成功");
        }
    }

    /**
     * 利用UnSafe类的CAS命令进行对state属性的原子操作
     *
     * @param expect 预期值
     * @param target 目标值
     * @return 成功true/失败false
     */
    private boolean compareAndSwapState(int expect, int target) {
        return unSafe.compareAndSwapInt(this, stateOffset, expect, target);
    }
}


  执行测试类,结果如下:

当前锁没有被占用,[线程0]尝试去获取锁
当前锁没有被占用,[线程1]尝试去获取锁
当前锁没有被占用,[线程0]尝试去获取锁
>>> [线程1]:获取锁成功
当前锁没有被占用,[线程2]尝试去获取锁
[线程0]:获取锁失败,阻塞等待前一个线程释放锁.
[线程2]:获取锁失败,阻塞等待前一个线程释放锁.
<<< [线程1]:释放锁成功
当前锁没有被占用,[线程0]尝试去获取锁
>>> [线程0]:获取锁成功
<<< [线程0]:释放锁成功
当前锁没有被占用,[线程2]尝试去获取锁
>>> [线程2]:获取锁成功
<<< [线程2]:释放锁成功


  本例中仅介绍了锁的基本实现原理,对于lock的可重入,非公平,可中断等特性没有介绍,有兴趣的同学可以去读读源码,700多行。


最后

  上面的手写Lock是使用一种通俗易懂的思想来解释下Lock实现,AQS实现的原理,会很粗糙,大家能够理解就好了,以后我能力提升了,会在细化内容,提炼精华,感谢大家!

感谢大家阅读,欢迎斧正

评论区
Rick ©2018