【手写系列】手写 AQS 实现 MyLock

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        int[] cnt = {1000};
        Runnable task = () -> {
            for (int i = 0;i < 100;i ++){
                LockSupport.parkNanos(10);
                cnt[0]--;
            }
        };

        List<Thread> threadList = new ArrayList<>();
        for (int i = 0;i < 10;i ++){
            Thread thread = new Thread(task);
            threadList.add(thread);
            thread.start();
        }
        for (Thread thread : threadList) {
            thread.join();
        }

        System.out.println(cnt[0]);
    }
}

上面代码是一个典型的线程不安全的例子,cnt[0] 的结果大概率不是 0,因为 cnt[0]--操作不是原子的。

如何解决呢? 对 cnt[0]-- 操作加锁,使之变为 原子操作即可。

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        int[] cnt = {1000};
        
        Lock lock = new ReentrantLock();
        Runnable task = () -> {
            lock.lock();
            for (int i = 0;i < 100;i ++){
                LockSupport.parkNanos(10);
                cnt[0]--;
            }
            lock.unlock();
        };

        List<Thread> threadList = new ArrayList<>();
        for (int i = 0;i < 10;i ++){
            Thread thread = new Thread(task);
            threadList.add(thread);
            thread.start();
        }
        for (Thread thread : threadList) {
            thread.join();
        }

        System.out.println(cnt[0]);
    }
}

现在我的需求:自己实现一个 Lock,完成上述功能。

最简单的实现方式:

java 复制代码
public class MyLock {

    // false: 没有线程获取锁
    AtomicBoolean flag = new AtomicBoolean(false);

    void lock(){
        while (true){
            // 成功从 false -> true,表示获取锁
            if (flag.compareAndSet(false,true)){
                return;
            }
        }
    }

    void unlock(){
        while (true){
            if (flag.compareAndSet(true,false)){
                return;
            }
        }
    }
    
}

上述 锁的实现方式,能够达成相同的效果。

思考两个问题:

  1. unlock(),不是持有锁的线程 释放锁 怎么办 ?
  • owner 变量 记录当前持有锁的线程,释放锁 前判断一下是否是持有锁的线程。
  1. lock() 方法,没有获取到锁线程,一直尝试获取锁,CPU 空转,并没有阻塞,如何解决?
  • 把 没有获取锁的线程 包装成 节点,放在 链表最后。
  • 阻塞,由 释放锁的线程 唤醒。
    • 唤醒链表中 第一个节点 的 线程,不会全部唤醒。 全部唤醒 又会进入 激烈的锁竞争中。

MyLock

java 复制代码
public class MyLock {

    AtomicBoolean flag = new AtomicBoolean(false);
    Thread owner;
    // dummy 节点
    AtomicReference<Node> head = new AtomicReference<>(new Node());
    AtomicReference<Node> tail = new AtomicReference<>(head.get());

    void lock(){
        if (flag.compareAndSet(false,true)){
            owner = Thread.currentThread();
            return;
        }
        // 没有获取到锁
        // 把线程包装成 Node,放在链表尾
        Node current = new Node();
        current.thread = Thread.currentThread();
        while (true){
            // CAS 加到链表
            Node currentTail = tail.get();
            if (tail.compareAndSet(currentTail,current)){
                current.pre = currentTail;
                currentTail.next = current;
                break;
            }
        }

        while (true){
            // 在真正地阻塞前,自己在最后获取一次锁
            // head --> A --> B
            if (current.pre == head.get() && flag.compareAndSet(false,true)){
                owner = Thread.currentThread();
                head.set(current);
                current.pre.next = null;
                current.pre = null;
                return;
            }
            LockSupport.park();
        }
    }

    void unlock(){
        if (Thread.currentThread() != owner){
            throw new IllegalStateException("当前线程并不持有锁!");
        }
        // 唤醒 head 下一个节点
        Node headNode = head.get();
        Node next = headNode.next;
        // 释放锁,之后是线程不安全的
        flag.set(false);
        if (next != null){
            LockSupport.unpark(next.thread);
        }
    }

    class Node{
        Node pre;
        Node next;
        Thread thread;
    }

}

问题 1:head、tail 为什么用 AtomicReference

  • 多线程环境下,引用改变 是线程不安全的。
  • 需要使用 AtomicReference 的 CAS 操作。

问题 2:

java 复制代码
Node current = new Node();
current.thread = Thread.currentThread();
// CAS 加到链表
while (true){
    // 多线程环境下,tail 是会变的。
    // 每次 CAS,都需要重新获取 tail。
    Node currentTail = tail.get();
    if (tail.compareAndSet(currentTail,current)){
        // 链表操作
        current.pre = currentTail;
        currentTail.next = current;
        break;
    }
}

问题 3:

java 复制代码
// 在真正地阻塞前,自己在最后获取一次锁
while (true){
    // head --> A(current) --> B
    // if (当前节点 是 第一个节点 && CAS获取锁成功)
    if (current.pre == head.get() && flag.compareAndSet(false,true)){
        owner = Thread.currentThread();

        // 设置 head
        // 不用CAS,因为获取锁的线程只会有一个
        head.set(current);
        
        // 断开 head <--> A 之间的连接
        current.pre.next = null;
        current.pre = null;
        return;
    }
    LockSupport.park();
}

下面的代码是说:阻塞的线程,只能等待其他线程的唤醒,才能够去获取锁。

  • 如果,没有其他线程唤醒的话,就一直阻塞下去。
  • 所以,在阻塞前,需要 获取一次锁,防止没有其他线程唤醒。
java 复制代码
while (true){
    LockSupport.park();
    if (current.pre == head.get() && flag.compareAndSet(false,true)){
        owner = Thread.currentThread();
        head.set(current);
        current.pre.next = null;
        current.pre = null;
        return;
    }
}

构建链表(节点连接)的过程 存在延迟,导致 持有锁的线程 没有获取到下一个需要唤醒的线程。

  • <font style="color:rgb(24, 25, 28);">currentTail.next = current</font> 还没有执行

此时若线程是先 park 的,则有可能导致线程无法被唤醒;

而先自己尝试解锁,失败了再 park ,可以在 持有锁的线程 唤醒失败的情况下自己获取锁,线程不会被park ,从而避免这一情况。

线程 1 解锁获取链表的 next 但此时获取到的是空,就不会唤醒线程。

如果 cpu 的时间片刚好给到了线程 2,那么此时线程 2 在链表中被阻塞,就无法唤醒了。

如果后面又进来了一个线程 3 也来获取这个锁,

  • 在公平锁的情况下,是会直接唤醒线程 2。
  • 如果是非公平锁的情况下,就是先是线程 3 获取到锁,然后再释放线程 3 的锁时(假设这个过程中也没有其他线程进来竞争锁了) 就唤醒线程2 了。

问题 4:

java 复制代码
void unlock(){
    if (Thread.currentThread() != owner){
        throw new IllegalStateException("当前线程并不持有锁!");
    }
   
    Node headNode = head.get();
    Node next = headNode.next;
    // 唤醒线程之前,释放锁
    // 不释放,会导致:线程被唤醒,获取不到锁,又阻塞了
    // 释放锁不需要 CAS。 因为一定是持有锁的线程,才能释放锁。
    flag.set(false);
    
    // 释放锁之后,线程就不安全了
    if (next != null){
        LockSupport.unpark(next.thread);
    }
}

问题 5:

MyLock 中获取锁的方式?

  • 直接获取锁。
  • 被唤醒后,获取锁。

非公平锁:获取锁的线程,不用放到 链表中,可以 直接获取锁。

公平锁:获取锁的线程,全部放到 链表 中,挨个唤醒头结点。

上述是 非公平锁的实现。

java 复制代码
if (flag.compareAndSet(false,true)){
    owner = Thread.currentThread();
    return;
}

去掉这块代码之后就是公平锁了。

测试 MyLock

java 复制代码
public static void main(String[] args) throws InterruptedException {
    int[] cnt = {1000};
    MyLock lock = new MyLock();
    Runnable task = () -> {
        lock.lock();
        for (int i = 0;i < 100;i ++){
            LockSupport.parkNanos(1);
            cnt[0]--;
        }
        lock.unlock();
    };

    List<Thread> threadList = new ArrayList<>();
    for (int i = 0;i < 10;i ++){
        Thread thread = new Thread(task);
        threadList.add(thread);
        thread.start();
    }
    for (Thread thread : threadList) {
        thread.join();
    }

    System.out.println(cnt[0]);
}
plain 复制代码
Thread-5加入到链表尾
Thread-7加入到链表尾
Thread-1加入到链表尾
Thread-2加入到链表尾
Thread-3加入到链表尾
Thread-4加入到链表尾
Thread-8加入到链表尾
Thread-9加入到链表尾
Thread-6加入到链表尾
Thread-0获取到锁
Thread-2被唤醒获取到锁
Thread-0唤醒了Thread-2
Thread-2唤醒了Thread-1
Thread-1被唤醒获取到锁
Thread-1唤醒了Thread-3
Thread-3被唤醒获取到锁
Thread-4被唤醒获取到锁
Thread-3唤醒了Thread-4
Thread-6被唤醒获取到锁
Thread-4唤醒了Thread-6
Thread-5被唤醒获取到锁
Thread-6唤醒了Thread-5
Thread-5唤醒了Thread-8
Thread-8被唤醒获取到锁
Thread-7被唤醒获取到锁
Thread-8唤醒了Thread-7
Thread-7唤醒了Thread-9
Thread-9被唤醒获取到锁
0

补充

(一)

主要原因在 ++链表链接++ 和 ++创建节点++ 的过程不是原子的

(二)

java 的 <font style="color:rgb(24, 25, 28);">park</font><font style="color:rgb(24, 25, 28);">unpark</font> 是没有先后的约束的,先<font style="color:rgb(24, 25, 28);">unpark</font><font style="color:rgb(24, 25, 28);">park</font><font style="color:rgb(24, 25, 28);">park</font>不住的,可以继续执行。

这是个标记,存在jvm层面的。 同时,多次 <font style="color:rgb(24, 25, 28);">unpark</font> 也只会标记 1 ,不会一直 +1.

(三)

  • 判断指向比 CAS 资源消耗少
  • 判断逻辑不光是在 ++阻塞被唤醒++ 的时候,还有 ++刚把自己插入队尾之后++ ,判断一下是不是可以不阻塞直接启动,判断引用比 CAS 消耗少
  • 虚假唤醒。虚假唤醒是CPU的问题,是可能完全无征兆的被唤醒,这个是应用程序(我们的代码)无法控制的,所以所有的 wait 永远都应该放在 while 里面,这是JDK注释中要求的

思考

java 复制代码
public class MyLock {

    AtomicInteger state = new AtomicInteger(0);
    Thread owner;
    AtomicReference<Node> head = new AtomicReference<>(new Node());
    AtomicReference<Node> tail = new AtomicReference<>(head.get());

    void lock(){
        // 没有线程持有锁
        if (state.get() == 0){
            // 成功 CAS -> 加锁成功
            if (state.compareAndSet(0,1)){
                owner = Thread.currentThread();
                System.out.println(Thread.currentThread().getName() + "获取到锁");
                return;
            }
        }else {
            // 持有锁的线程是否是自己
            if (Thread.currentThread() == owner){
                state.set(state.get() + 1);
                return;
            }
        }

        Node current = new Node();
        current.thread = Thread.currentThread();
        while (true){
            Node currentTail = tail.get();
            if (tail.compareAndSet(currentTail,current)){
                current.pre = currentTail;
                currentTail.next = current;
                System.out.println(Thread.currentThread().getName() + "加入到链表尾");
                break;
            }
        }

        while (true){
            if (current.pre == head.get() && state.compareAndSet(0,1)){
                owner = Thread.currentThread();
                head.set(current);
                current.pre.next = null;
                current.pre = null;
                System.out.println(Thread.currentThread().getName() + "被唤醒获取到锁");
                return;
            }
            LockSupport.park();
        }
    }

    void unlock(){
        if (Thread.currentThread() != owner){
            throw new IllegalStateException("当前线程并不持有锁!");
        }
        
        // 重入的次数
        int stateCnt = state.get();
        if (stateCnt > 1){
            state.set(stateCnt - 1);
            return;
        }
        if (stateCnt <= 0){
            throw new IllegalStateException("重入锁 解锁错误!!!");
        }

        // i = 1
        Node headNode = head.get();
        Node next = headNode.next;
        owner = null;
        state.set(0);
        if (next != null){
            LockSupport.unpark(next.thread);
            System.out.println(Thread.currentThread().getName() + "唤醒了" + next.thread.getName());
        }
    }

    class Node{
        Node pre;
        Node next;
        Thread thread;
    }

}

解释 1:

java 复制代码
Node headNode = head.get();
Node next = headNode.next;
owner = null;
state.set(0);

++如果不把 owner 置 null++,会在以下情况出问题:

  • 线程A 解了锁,然后线程B 秒抢锁,但还没来得及把 owner 改成B,就被 A 重入了,然后B 才执行 owner = B;
  • 这时候 A,B 都有锁,A执行 unlock 的时候才发现锁是B的,就抛异常了
相关推荐
sg_knight28 分钟前
Eureka 高可用集群搭建实战:服务注册与发现的底层原理与避坑指南
java·spring boot·spring·spring cloud·微服务·云原生·eureka
钟离墨笺29 分钟前
Go语言学习-->编译器安装
开发语言·后端·学习·golang
why15135 分钟前
百度golang研发一面面经
开发语言·golang
钟离墨笺2 小时前
Go语言学习-->从零开始搭建环境
开发语言·后端·学习·golang
whoarethenext2 小时前
使用 C++/OpenCV 图像直方图比较两个图片相似度
开发语言·c++·opencv·直方图·相似度对比
csdndenglu3 小时前
QT 5.9.2+VTK8.0实现等高线绘制
开发语言·qt
某某3 小时前
DashBoard安装使用
大数据·开发语言·kubernetes
@Turbo@3 小时前
【QT】QString& 与QString区别
开发语言·qt
数据潜水员6 小时前
C#基础语法
java·jvm·算法
明月看潮生6 小时前
青少年编程与数学 02-020 C#程序设计基础 15课题、异常处理
开发语言·青少年编程·c#·编程与数学