手写一个模拟的ReentrantLock

package cn.daheww.demo.juc.reentrylock;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.locks.LockSupport;

/**
 * @author daheww
 * @date 2022/7/7
 */
public class MiniReentryLock implements Lock {

    /**
     * 锁的是什么 --> 资源 --> state
     * 0 --> 未加锁
     * >0 -> 加锁
     */
    private volatile int state;

    /**
     * 独占模式
     * 同一时刻只有一个线程可以持有锁,其它线程在未获取到锁的时候会被阻塞
     *
     * 当前独占锁的线程(占用锁的线程)
     */
    private Thread exclusiveOwnerThread;

    /**
     * 需要有两个节点去维护阻塞队列
     * Head 指向队列的头节点
     * Tail 指向队列的尾节点
     *
     * 比较特殊:Head节点对应的线程就是当前占用锁的线程
     */
    private Node head;
    private Node tail;

    /**
     * 获取锁
     * 假设当前锁被占用,则会阻塞调用者线程,直到它抢占到锁为止
     *
     * 模拟公平锁
     * --> 先来后到
     *
     * lock的过程
     * 情景1.线程进来后发现,当前state == 0 --> 直接去抢锁
     * 情景2.线程进来后发现,当前state > 0 --> 将当前线程入队
     */
    @Override
    public void lock() {
        // 第一次获取到锁时,将state设置为1
        // 第n次重入时,将state设置为n
        acquire(1);
    }

    @Override
    public void unlock() {
        release(1);
    }

    private void release(int arg) {
        // 条件成立:说明线程已经完全释放锁了
        if (tryRelease(arg)) {
            // 阻塞队列里面,还有睡觉的线程,应该唤醒一个线程
            // 首先需要知道有没有等待的node --> head.next == null
            Node head = this.head;
            if (head.nx != null) {
                // 公平锁,唤醒head.nx节点
                unparkSuccessor(head);
            }
        }
    }

    private void unparkSuccessor(Node node) {
        Node s = node.nx;

        if (s != null && s.thread != null) {
            LockSupport.unpark(s.thread);
        }
    }

    /**
     * 完全释放锁成功则返回true
     */
    private boolean tryRelease(int arg) {
        int c = getState() - arg;

        if (getExclusiveOwnerThread() != Thread.currentThread()) {
            throw new RuntimeException("must get lock first");
        }

        // 如果执行到这里,不存在并发,只会有一个线程会来到这里
        // 条件成立,则说明当前线程持有的lock锁已经完全释放了
        if (c == 0) {
            this.exclusiveOwnerThread = null;
            this.state = c;
            return true;
        } else {
            this.state = c;
            return false;
        }
    }

    /**
     * 竞争资源
     * 1.尝试获取锁。成功则占用锁,且返回
     * 2.抢占锁失败,阻塞当前线程
     * @param arg
     */
    private void acquire(int arg) {
        if (!tryAcquire(arg)) {
            // 抢锁失败

            // step1.将当前线程封装成node,加入到阻塞队列中
            Node node = addWaiter();
            // step2.将当前线程park,使线程处于挂起状态
            acquireQueued(node, arg);
        }

        // 抢锁成功
        // 1.抢到了锁
        // 2.重入了锁
    }

    /**
     * 尝试抢锁失败,需要做的事:
     * 1.需要将当前线程封装成node,加入到阻塞队列中
     * 2.需要将当前线程park,使线程处于挂起状态
     *
     * 唤醒的流程:
     * 1.检查当前node是否为head.next节点
     *      head.next是拥有抢占权限的线程,其它node都没有抢占的权限
     * 2.抢占:
     *      成功:
     *          1.将当前node设置为node,将老的head出队,返回到业务层面
     *          2.继续park等待被唤醒
     *
     * ----------------------------------------------
     * 1.添加到阻塞队列的逻辑 addWaiter()
     * 2.竞争资源的逻辑      acquireQueued()
     */
    private void acquireQueued(Node node, int arg) {
        // 当前线程已经放到queue中了

        // 只有当前node成功获取到锁以后才会跳出自旋
        for (; ; ) {
            // 什么情况下,当前node被唤醒后可以尝试去获取锁呢?
            // 只有一种情况,当前node是head的后继节点,才有这个权限
            // 不是就先来后到

            Node pvNode = node.pv;
            // 条件1:pvNode == head
            //      true --> 说明当前node拥有抢占权限
            //               queue中的第一个节点代表的是当前锁正在执行的线程 --> head指向的线程
            //               head后面的线程代表的是正在排队的线程 --> 所以只有head.nx节点拥有抢锁的权利
            // 条件2:tryAcquire(arg)
            //      true --> 当前线程获取到了锁
            //
            if (pvNode == head && tryAcquire(arg)) {
                // 进入到这里面说明当前线程竞争锁成功了
                // 需要做的操作:
                // 1.设置当前head为当前线程的node
                // 2.协助原来的对象出队
                setHead(node);
                pvNode.nx = null;
                // 因为获取到了锁,所以就return了
                return;
            }

            // 当前不是head.nx节点,或者去尝试获取锁失败了,这个时候都需要去把当前线程park掉
            System.out.println("线程:" + Thread.currentThread().getName() + " 挂起");
            LockSupport.park();
            // 直到某个线程做了当前线程的unPark操作,这个线程才会继续执行
            /*
                所以总结一下,lock的逻辑就是:
                   1.在没锁的情况下,如果有个线程调用了lock方法,它就会改变lock中的state值。此时state值就不会为0了。
                   那么其它线程调用lock方法时,会看到这个state不为0。
                   2.然后这个线程会被封装成一个node节点
                   3.然后会去尝试竞争一下锁,做一下最后的挽救工作,如果实在挽救不了,就park了
                     --> 线程就在这个lock的lock()方法里被阻塞了。就达到了锁的效果
                     --> 所有调用这个锁对象lock的方法只能有一个线程能继续执行,然后其它线程会被阻塞,直到这个线程做了unlock操作
             */
            System.out.println("线程:" + Thread.currentThread().getName() + " 唤醒");

            // 什么时候唤醒被park的线程?--> unlock()
        }
    }

    /**
     * 把当前线程入队
     * 返回当前线程对应的node节点
     *
     * addWaiter执行完成后,保证当前线程已经入队成功
     */
    private Node addWaiter() {
        Node newNode = new Node(Thread.currentThread());

        // 如何入队?
        // Case1.当前node不是第一个入队的node,队列已经有等待的node了
        //     1.找到newNode的pv节点
        //     2.更新newNode.pvNode = pv节点
        //     3.CAS更新tail为newNode
        //     4.更新pv节点
        Node pvNode = tail;
        if (pvNode != null) {
            newNode.pv = pvNode;
            // 条件成立,说明当前线程成功入队
            if (compareAndSetTail(pvNode, newNode)) {
                pvNode.nx = newNode;
                return newNode;
            }
        }

        // 执行到这里的几种情况
        // 1.tail == null队列是空
        // 2.cas设置当前newNode为tail时失败了 --> 循环入队 --> 自旋
        enq(newNode);

        return newNode;
    }

    /**
     * 自旋入队,只有成功之后才返回
     * 1.tail == null 队列是空队列
     * 2.cas设置当前newNode为tail时失败了
     */
    private void enq(Node node) {
        for (; ; ) {
            // 第一种情况:队列是空队列
            // --> 当前线程是第一个抢占锁的线程...

            // 当前持有锁的线程,并没有设置过任何node,所以作为该线程的第一个后驱节点
            // 需要给他擦屁股
            // 给当前持有锁的线程补充一个node作为head节点
            // head节点任何时候都代表当前占用锁的线程
            if (tail == null) {
                // 条件成立:说明当前线程给当前持有锁的线程补充head操作成功了
                if (compareAndSetHead(new Node())) {
                    tail = head;
                    // 注意,并没有直接返回,而是会继续自旋
                }
            } else {
                // 当前队列中已经有node了,说明这是一个追加node的过程

                // 如何入队呢?
                //     1.找到newNode的pv节点 --> 最新的tail节点
                //     2.更新newNode.pvNode = pv节点
                //     3.CAS更新tail为newNode
                //     4.更新pv节点
                Node pvNode = tail;
                node.pv = pvNode;
                // 条件成立,说明当前线程成功入队
                if (compareAndSetTail(pvNode, node)) {
                    pvNode.nx = node;
                    return;
                }
            }
        }
    }

    /**
     * 尝试获取锁,不会去阻塞线程
     * true --> 抢占成功
     * false --> 抢占失败
     */
    private boolean tryAcquire(int arg) {
        if (state == 0) {
            // 当前state为0
            // 不能直接抢锁 --> 公平锁 --> 先来后到
            // 条件一:!hasQueuedPredecessors() ---> 取反之后为true,表示当前线程前面没有等待着的线程
            // 条件二:compareAndSetState(0, arg) -> 使用cas的原因:lock方法可能有多线程调用的情况
            //      true --> 当前线程抢锁成功
            //          (1) volatile --> state被volatile修饰了,所以其它线程能第一时间知道这个值不为0了 --> 缓存能够一致了
            //          (2) cas -------> state从0变为arg的操作用cas实现,用于保证只会有一个线程能够改变state的值(0->arg) --> 只会有一个线程能够执行接下来的操作 --> 锁
            //                1.如果cas的变量不用volatile修饰就没有意义:
            //                   因为A线程改变了state的值,但是B线程并不知道
            //                  (可见性,volatile会让B线程中的副本马上失效,然后获取最新的state的值,此时B线程工作空间中的state值就不为0了)
            //                2.如果volatile的变量不用cas去改变它的值的话,也没有意义:
            //·                  step1.A线程,B线程都拿到了state的副本信息,此时state值为0
            //                   step2.A线程改变了state的值。B线程还在写,因为state的值改变了,所以B线程工作空间中的state值改变,然后B继续写。
            //                   所以所有判断出state值为0的线程都能写成功,并且能执行写成功后续的操作
            //                所以要用cas+volatile去保证只会有一个线程能够写成功这个值
            //                Ps.可以看到,如果这些线程想写的值都是同一个值的话,多写了几次,但是结果和只写一次是一致的
            //                   cas+volatile主要还是去控制写成功之后的操作只会被执行一次,这样就像一个锁一样了
            if (!hasQueuedPredecessors() && compareAndSetState(0, arg)) {
                // 抢锁成功
                // 1.将exclusiveOwnerThread设置为当前线程
                this.exclusiveOwnerThread = Thread.currentThread();
                return true;

                // 不会入队任何node,接返回true
                // 接下来第一个竞争失败的线程会先去帮忙创建一个node,然后再执行后续的操作
            }
            // 当前线程前面有线程在等待 || 多个线程和当前线程一起在尝试获取这个锁,然后当前线程失败了 --> return false;
        } else if (Thread.currentThread() == this.exclusiveOwnerThread) {
            // 执行的时机:
            // 1.当前锁被占用
            // 2.当前线程即为持锁线程

            // 这里面不存在并发。只有当前加锁的线程才有权限修改state
            //   即使是同一个线程多次进入到这,设置state的值,那么它们都是使用的同一个工作空间
            //   不存在不同工作空间下,这个值的不一样的情况(因为没有了缓存)

            // 锁重入的流程

            int c = getState();
            c += arg;
            // TODO 越界判断
            this.state = c;
            return true;
        }

        // 什么时候会返回false?
        // 1.cas加锁失败
        // 2.state大于0,且当前线程不是持锁线程
        return false;
    }

    /**
     * 当前线程前面是否有等待着的线程
     * true --> 当前线程前面有等待着的线程
     * false -> 当前线程前面没有其它等待着的线程
     *
     * 调用链
     * lock --> acquire -> tryAcquire -> hasQueuedPredecessors(state值为0时,即当前lock为无主状态)
     *
     * 什么时候返回false?
     * 1.当前队列是空
     * 2.当前线程为head.next节点 --> head.next在任何时候都有权力去争取lock
     */
    private boolean hasQueuedPredecessors() {
        Node h = head;
        Node t = tail;
        Node s;

        // 条件一:h != t
        //     true --> 当前队列已经有node了
        //     false -> h == t
        //          case1. h == t == null --> 还没初始化过queue
        //          case2. h == t == head
        //              第一个获取锁失败的线程会为当前持有锁的线程补充创建一个head node
        // 条件二:
        //     前置条件:条件一成立
        //     排除几种情况:
        //       条件2.1:极端情况 --> 第一个获取锁失败的线程,会为持锁的线程补充创建head节点,然后在自旋入队
        //                           step1.cas设置tail成功了
        //                           step2.head.next = node
        //                           在这两步中间的时候,有线程来检查前面是否有等待的线程
        //               这种情况应该返回true:已经有head.next节点了,其它线程来这的时候需要返回true
        //       条件2.2:
        //               前置条件:h.next不是null
        //               true --> 条件成立说明当前线程就是持有锁的线程
        //               false -> 说明当前线程就是h.next节点对应的线程,需要返回false。回头线程就会去竞争锁了

        return h != t && ((s = h.nx) == null || s.thread != Thread.currentThread());
    }

    private static final Unsafe UNSAFE;
    private static final long STATE_OFFSET;
    private static final long HEAD_OFFSET;
    private static final long TAIL_OFFSET;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);

            UNSAFE = (Unsafe) f.get(null);
            STATE_OFFSET = UNSAFE.objectFieldOffset(MiniReentryLock.class.getDeclaredField("state"));
            HEAD_OFFSET = UNSAFE.objectFieldOffset(MiniReentryLock.class.getDeclaredField("head"));
            TAIL_OFFSET = UNSAFE.objectFieldOffset(MiniReentryLock.class.getDeclaredField("tail"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    private boolean compareAndSetHead(Node update) {
        return UNSAFE.compareAndSwapObject(this, HEAD_OFFSET, null, update);
    }

    private boolean compareAndSetTail(Node expect, Node update) {
        return UNSAFE.compareAndSwapObject(this, TAIL_OFFSET, expect, update);
    }

    private boolean compareAndSetState(int expect, int update) {
        return UNSAFE.compareAndSwapInt(this, STATE_OFFSET, expect, update);
    }

    /**
     * 阻塞的线程被封装成node节点,然后放进FIFO队列
     */
    static final class Node {
        /**
         * 封装的线程本身
         */
        Thread thread;
        /**
         * 前置节点引用
         */
        Node pv;
        /**
         * 后置节点引用
         */
        Node nx;

        public Node(Thread thread) {
            this.thread = thread;
        }

        public Node() {
        }
    }

    public int getState() {
        return state;
    }

    private void setHead(Node node) {
        this.head = node;
        // 当前线程已经是获取到锁的线程
        node.thread = null;
        node.pv = null;
    }

    public void setState(int state) {
        this.state = state;
    }

    public Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }

    public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {
        this.exclusiveOwnerThread = exclusiveOwnerThread;
    }

    public Node getHead() {
        return head;
    }

    public Node getTail() {
        return tail;
    }

    public void setTail(Node tail) {
        this.tail = tail;
    }

}

Original: https://www.cnblogs.com/daheww/p/16456275.html
Author: daheww
Title: 手写一个模拟的ReentrantLock

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/575182/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Redmi AC2100 路由器 官方固件允许IPv6外网访问下游设备

    开启SSH权限。F12打开浏览器的开发者模式,并切换至终端选项卡,复制以下代码至终端处,并敲回车执行: function getSTOK() { let match = locat…

    Java 2023年6月7日
    094
  • 你说说对Java中SPI的理解吧

    前言 最近在面试的时候被问到SPI了,没回答上来,主要也是自己的原因,把自己给带沟里去了,因为讲到了类加载器的双亲委派模型,后面就被问到了有哪些是破坏了双亲委派模型的场景,然后我就…

    Java 2023年5月29日
    083
  • 【Redis】WRONGTYPE Operation against a key holding the wrong kind of value

    此异常的出现情况之一:第一次为 key 设置 value 值是字符串类型,第二次 为 相同 key 设置 value 值的类型不同。 posted @2022-09-28 20:5…

    Java 2023年6月15日
    080
  • iOS的Runtime知识点繁杂难啃,真的理解它的思想,你就豁然开朗了

    一、Runtime 1、概念: 概念:Runtime是Objective-c语言动态的核心,即运行时。在面向对象的基础上增加了动态运行,达到很多在编译时确定方法推迟到了运行时,从而…

    Java 2023年6月16日
    071
  • JVM快速扫盲篇

    JVM虚拟机基础 JVM虚拟机结构 jvm的整体结构大致如下: 类加载器:类加载器用来加载Java类到JVM虚拟机中,源代码程序.java文件在经过编译器编译之后就被转换成字节代码…

    Java 2023年6月5日
    091
  • CSharp: State Pattern

    csharp;gutter:true; /// ///empty base class containing State methods to override /// State…

    Java 2023年6月16日
    079
  • 转载:Spring+EhCache缓存实例

    转载来自:http://www.cnblogs.com/mxmbk/articles/5162813.html 一、ehcahe的介绍 EhCache 是一个纯Java的进程内缓存…

    Java 2023年5月30日
    079
  • Kafka 是如何做到消息不丢或不重复的

    *消息重复。 相同的消息重复发送会造成消费者消费两次同样的消息,这同样会造成系统间数据的不一致。比如,订单支付成功后会通过消息队列给支付系统发送需要扣款的金额,如果消息发送两次一样…

    Java 2023年6月9日
    070
  • 全面解决.Net与Java互通时的RSA加解密问题,使用PEM格式的密钥文件

    一、缘由 RSA是一种常用的非对称加密算法。所以有时需要在不用编程语言中分别使用RSA的加密、解密。例如用Java做后台服务端,用C#开发桌面的客户端软件时。由于 .Net、Jav…

    Java 2023年5月29日
    072
  • mybatis

    基于JDBC的。 public class JDBCDemo { public static void main(String[] args) throws SQLExceptio…

    Java 2023年5月30日
    076
  • Spring Bean的作用域

    Spring Bean的作用域或者说范围主要有五种: 作用 描述 singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,bean作用域范围的…

    Java 2023年6月14日
    085
  • Java动态代理

    在我们日常开发中,代理模式是一个非常常见的模式。动态代理时jdk中自带的,可以非常方便的在原有的功能上添加一些我们自己的功能。 什么是代理 就是为其他对象提供一个代理以控制被代理对…

    Java 2023年6月7日
    0123
  • centos使用openssl生成自签名SSL证书并配置到nginx

    检查OpenSSL 检查是否已经安装openssl: 一般在CentOS7上,openssl已经默认安装好了。 生成自签名的SSL证书和私钥 新建/etc/ssl/certs/ww…

    Java 2023年5月30日
    0163
  • 【Java面试手册-基础篇】如何实现在main()方法执行前输出”Hello World”?

    在Java语言中,main()方法是程序的入口方法,在程序运行时,最先加载的就是main()方法,但这是否意味着main()方法就是程序运行时第一个被执行的模块呢? 答案不是的,在…

    Java 2023年6月8日
    087
  • Java接口和抽象类区别

    接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 接口中除了 static、final 变量,不…

    Java 2023年5月29日
    091
  • Spring Boot + Spring Cloud 构建微服务系统(九):配置中心(Spring Cloud Config)

    技术背景 如今微服务架构盛行,在分布式系统中,项目日益庞大,子项目日益增多,每个项目都散落着各种配置文件,且随着服务的增加而不断增多。此时,往往某一个基础服务信息变更,都会导致一系…

    Java 2023年5月30日
    062
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球