剖析AQS后,决定手写一个可重入锁

5 篇文章 1 订阅

坚持看完相信你会有收获

可重入锁, 表示同一个线程可重复对某个资源进行上锁. 下面为了更好的理解可重入锁, 先来了解下不可重入锁.

简单写了示例帮助了解不可重入锁

// 在Java中, 可以很好的设计一种不可重入锁.
class NonReentrantLock {
        private volatile boolean isLock;
        private Thread ownerThread;

        // 获取锁的方法
        public synchronized void lock() {
            // 判断锁的同时没有判断获取锁的线程, 导致线程重复获取锁会睡眠
            // 关键的地方就是下面的while循环.
            while (isLock) {
                try {
                    // 让线程进入睡眠的方法.
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ownerThread = Thread.currentThread();
            isLock = true;
        }

        // 释放锁的方法
        public synchronized void unlock() {
            if (ownerThread != null && ownerThread == Thread.currentThread()) {
                isLock = false;
                notifyAll();
            } else {
                throw new IllegalMonitorStateException();
            }
        }
    }
}

上面的代码是简单写了一个不可重入锁. 当其他线程试图获取锁或已经获取锁的线程再次获取锁时, 会一直阻塞, 最后导致线程睡眠.

对上面的代码进行改造, 使其成为可重入锁.

private static class MyReentrantLock {
    // 查看是否已经上锁.
    private volatile boolean isLock;
    private Thread ownerThread;
    // 查看锁状态.
    private volatile int state;
    
    // 获取锁
    public synchronized void lock() {
        // 获取锁时, 通过判断已经占用锁的线程是否为当前线程
        while (isLock && ownerThread != Thread.currentThread()) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        ownerThread = Thread.currentThread();
        isLock = true;
        state++;
    }
    
    // 释放锁
    public synchronized void unlock() {
        if (ownerThread != null && ownerThread == Thread.currentThread()) {
            isLock = false;
            state--;
            if (state == 0) {
                notifyAll();
            }
        } else {
            throw new IllegalMonitorStateException();
        }
    }
}

可以自己写几个线程, 对其进行测试, 同一个线程对其加锁, 并不会阻塞.

上面的可重入锁是一个粗糙的版本, 有很多需要优化的地方. 接下来逐步优化, 使其变得更强大.

第一步优化, 先把isLock变量去掉. 因为该变量再加入state变量之后就有点多余. 使用state变量足以表示锁的状态.

  1. state == 0 表示锁闲, 没有线程占用.
  2. state > 0 表示锁忙, 数字的大小表示加锁的次数.

删除isLock成员变量, 并且修改lock方法和unlock方法.

private static class MyReentrantLock {
    private Thread ownerThread;
    // 锁状态.
    private volatile int state;
    
    // 获取锁
    public synchronized void lock() {
        // 通过state变量进行判断锁状态
        while (state > 0 || ownerThread != Thread.currentThread()) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        thread = Thread.currentThread();
        state++;
    }
    
    // 释放锁
    public synchronized void unlock() {
        // 通过ownerThread判断当前是否有线程占用
        if (ownerThread != null && ownerThread == Thread.currentThread()) 
            state--;
            if (state == 0) {
                notifyAll();
            }
        } else {
            throw new IllegalMonitorStateException();
        }
    }
}

现在代码看起来清晰了, 但是不高效. 因为依赖synchronized关键字保证并发抢锁的正确性. 关于 synchronized 为什么不高效,以及运行原理先不在这里展开述说了.

关于怎么把synchronized去掉, 换成一个更高效的方式来保证线程并发抢锁正确呢?

可以引入CAS(CompareAndSwap), 先对其简单介绍一下: 假设有一个变量money, 其值为100, 现在A线程期望修改成200, B线程期望修改成300. 两个线程同时cas修改money变量, 则操作系统能保证只有一个线程会成功, 另一个线程会失败. 成功的线程返回true, 失败的线程返回false.

为什么要引入CAS呢?
多个线程同时去抢锁的时候, 可以不让其进入睡眠. 减少线程上下文切换从而提高性能.

要想在java中使用cas操作, 需要通过Unsafe类, 如何使用Unsafe这里就不展开叙述了. 可以自行查看美团技术团队出品的文章.

假设现在已经有如下可用的CAS操作, 不用纠结它是怎么实现的, 只需要知道它能完成上面提到原子功能.

// compareAndSetState 方法签名如下
// 对state变量进行cas赋值操作
private final boolean compareAndSetState(int expect, int update);

尝试使用compareAndSetState重写lock()方法和unlock()方法.

第一次可能写出是下面这个样子:

public void lock() {
    for(;;) {
        // 获取当前锁的状态
        int currentState = state;
        // 如果锁是空闲, 尝试cas设置锁状态
        if (currentState == 0 && compareAndSetState(currentState, currentState + 1)) {
            // 设置成功, 则保存获取锁的线程
            ownerThread = Thread.currentThread();
            return;
        // 如果锁忙, 则判断是否当前线程获取锁
        } else if (ownerThread == Thread.currentThread()) {
            // 当前线程占用锁, 修改状态. 这里没有并发, 直接赋值就可以.
            state++;
            return;
        }
        // 不满足上面的条件,则会进入下一次for循环.
    }
}

public void unlock() {
    // 判断是否获取锁的线程
    if (ownerThread != null && ownerThread == Thread.currentThread()) {
        // 是获取锁的线程, 修改state状态
        state--;
        // 判断是否释放锁
        if (state == 0) {
            // 将上次保存的线程清除.
            thread = null;
        }
    } else {
        throw new IllegalMonitorStateException();
    }
}

现在感觉一切很完美, 因为已经实现了主体功能, 但是有比较严重的问题.

  1. lock方法不让线程睡眠, 存在大量并发情况将使用很多CPU资源.
  2. unlock方法中, 对thread修改的地方也有并发情况, 假设unlock中已经调用了state--, 并且操作完成之后state等于0, 而另一个线程这时调用了lock方法, 并且lock方法成功了, 在lock方法中设置的ownerThread为调用lock的线程. 而unlock中会清除调用lock的线程, 导致错误产生.

修复上面的第二个问题比较容易. 在lock方法获取锁的时候, 判断占用线程是否为null, 并且可以给ownerThreadvolatile修饰. 保证其可见性.

🤔思考: 不对ownerThreadvolatile修饰是否有问题?
欢迎评论讨论.

public void lock() {
    for(;;) {
        int currentState = state;
        // 只需要将下面的判断代码修改下. 并且用volatile修饰ownerThread
        if (ownerThread == null && currentState == 0 && compareAndSetState(currentState, currentState + 1)) {
            ownerThread = Thread.currentThread();
            return;
        } else if (ownerThread == Thread.currentThread()) {
            state++;
            return;
        }
    }
}

现在已经解决了第二个问题, 下面解决第一个问题, 若想减少对CPU的浪费, 则需要让获取不到锁的线程进入睡眠状态, 等待占用锁的线程使用完成锁之后, 唤醒睡眠的线程.

因此问题发生了变化:

  1. 如何让线程进入睡眠, 并且唤醒
  2. 如何保存进入睡眠的线程

来看第一个问题, 如果让线程进入睡眠, 并且唤醒. 使用waitnoitfy并不可行, 具体原因可以自行分析.

那么是否可以使用sleep(long)方法呢? 我觉得是可以的, sleep方法指定一个时间, 可以给其Long.MAX_VALUE, 睡眠三亿年, 相当于永久睡眠了, 唤醒需要调用thread.interrupt()方法.

为什么在AQS中没有使用sleep(long)方法呢?
首先使用sleep(long)方法并不优雅, 并且interrupt()方法需要在sleep(long)方法调用之后调用才能唤醒. 若先调用interrupt方法, 再调用sleep方法, 则不会对线程进行唤醒(不会触发受检异常).

有一种使线程进入睡眠的方法是LockSupport#park(), 它是基于Unsafe实现的. LockSupport 非常简单易用, 使用起来比sleep(long)更优雅. 可以理解为LockSupport维护一个permit,其值最大为1, 调用park将permit减一, 调用unpark(Thread)线程睡眠的方法, 将permit加一. 开始每个线程permit为0, 调用park(Thread)先将permit减一, 之后判断permit是否小于0, 若小于0, 则进入睡眠, 等待其他线程调用unpark(Thread)唤醒线程的方法, 使permit加一后, 将睡眠的线程唤醒. 需要注意连续多次调用unpark(Thread), permit也是1.

总体来说, LockSupport提供的睡眠唤醒方法能够解决sleep(long)方法不优雅的问题.

现在来说第二个问题, 答案看似比较简单, 直接用链表保存即可. 但是里面涉及到并发, 就会使简单的问题不再简单.

假设将需要睡眠的线程封装为Node, 它里面有一个指向下一个Node的引用, 未来Node中还可以保存一些状态. 这样就会形成一个等待的链表, 而比较难的地方就在于多线程并发修改链表的过程, 也是多线程抢锁和释放锁的过程.

线程封装为链表中的节点.

private static class Node {
    Thread thread;
    Node next;
    Node(Thread thread) {
        this.thread = thread;
    }
}

下面直接来看代码, 调用获取锁的lock()方法中, 只要抢锁失败则会加入到链表中, 等待被唤醒抢锁, 并且线程被唤醒的顺序就是加入的顺序. 此外unlock()方法中, 只有获取锁的线程调用才不会抛出异常. 因此可以确保线程安全.

private static class MyReentrantLock {
    private volatile int state;
    private volatile Thread ownerThread;
    private volatile Node head;
    private volatile Node tail;
    
    // 获取锁
    public void lock() {
        for (;;) {
            int currentState = state;
            if (currentState == 0 && compareAndSetStatus(currentState,currentState + 1)) {
                ownerThread = Thread.currentThread();
                return;
            } else if(currentState > 0 && ownerThread == Thread.currentThread()) {
                state++;
                return;
            } else {
                // 加入链表中,并且睡眠等待唤醒
                addQueue();
                LockSupport.park();
            }
        }
    }
    
    // 添加到链表中
    private void addQueue() {
        Node node = new Node(Thread.currentThread());
        // 队列还为空
        for (;;) {
            Node tail = this.tail;
            if (tail == null) {
                // 初始化队列
                if (compareAndSetHead(node)) {
                    this.tail = node;
                    return;
                }
            } else {
                // 添加到队尾
                if (compareAndSetTail(tail, node)) {
                    tail.next = node;
                    return;
                }
            }
        }
    }
    
    // 释放锁
    public void unlock() {
        if (ownerThread != Thread.currentThread()) {
            throw new IllegalMonitorStateException();
        }
        if (--state == 0) {
            // 唤醒下一个抢锁的线程.
            ownerThread = null;
            unparkSuccessor();
        }
    }
    
    // 唤醒下一个线程
    private void unparkSuccessor() {
        Node head = this.head;
        if (head != null) {
            this.head = head.next;
            LockSupport.unpark(head.thread);
        }
    }
    
    // 内部对等待线程的数据抽象
    private static class Node {
        Thread thread;
        Node next;
        Node(Thread thread) {
            this.thread = thread;
        }
    }
    
    // 下面代码只看方法名字
    // 封装只是为了进行CAS操作
    private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            return null;
        }
    }
    private static final Unsafe UNSAFE = getUnsafe();
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;
    static {
        try {
            stateOffset = UNSAFE.objectFieldOffset(MyReentrantLock.class.getDeclaredField("state"));
            headOffset = UNSAFE.objectFieldOffset(MyReentrantLock.class.getDeclaredField("head"));
            tailOffset = UNSAFE.objectFieldOffset(MyReentrantLock.class.getDeclaredField("tail"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    private boolean compareAndSetStatus(int expect, int update) {
        return UNSAFE.compareAndSwapInt(this, stateOffset, expect, update);
    }
    private boolean compareAndSetHead(Node update) {
        return UNSAFE.compareAndSwapObject(this, headOffset, null, update);
    }
    private boolean compareAndSetTail(Node expect, Node update) {
        return UNSAFE.compareAndSwapObject(this, tailOffset, expect, update);
    }
}

上面代码还有一些问题
比如当释放锁的时候, 有个线程也来获取锁, 则链表中的等待线程怎么运行?
欢迎读者评论提出自己的想法(思考的过程,就是提高的过程), 以及如何可以更好提高该重入锁, 最后再去看AQS的源码. 这时候有一首诗特别适合 <<题西林壁>> 横看成岭侧成峰,远近高低各不同.

结束语

关于AQS, 还写了一篇文章,欢迎大家前去提提意见. AQS可以查看该文章.

创作不宜, 点个赞再走呗~ 可怜的目光~~~

文章总结:

  1. 线程睡眠的方式
  2. CAS操作的封装
  3. 并发情况操作链表(队列)
  4. 可重入锁的基本实现思路
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值