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;
} }

手写一个模拟的ReentrantLock的更多相关文章

  1. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  2. 只会用就out了,手写一个符合规范的Promise

    Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...

  3. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  4. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  5. 『练手』手写一个独立Json算法 JsonHelper

    背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...

  6. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  7. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  8. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  9. 手写一个简单的ElasticSearch SQL转换器(一)

    一.前言 之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql 插件, ...

随机推荐

  1. Java 18 新特性:简单Web服务器 jwebserver

    在今年3月下旬的时候,Java版本已经更新到了18.接下来DD计划持续做一个系列,主要更新从Java 9开始的各种更新内容,但我不全部都介绍,主要挑一些有意思的内容,以文章和视频的方式来给大家介绍和学 ...

  2. C#使用进程调用bash,不断往bash内插入命令并执行

    呃呃呃,遇到了一个非常恶心得问题,现在就是,有几十万条数据需要当作参数调用python脚本 一次一次输入命令过去缓慢,经过了多层考虑决定使用进程来解决(不知道线程怎么开启bash得) 原理非常简单,类 ...

  3. 用户与安全 -(1)Linux用户及组管理

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 前言 Linux 是多用户多任务操作系统,换句话说,Linux 系统支持多个用户在同一时间内登 ...

  4. Vue.js 3.x 双向绑定原理

    什么是双向绑定? 废话不多说,我们先来看一个 v-model 基本的示例: <input type="text" v-model="search"> ...

  5. acunetix_14.7安装破解

    acunetix_14.7.220401065版 本次更新增加了许多的漏洞检测,包括Spring4Shell漏洞(CVE-2022-22965) 下载地址: https://pan.baidu.com ...

  6. MySQL存储过程入门了解

    0.环境说明: mysql版本:5.7 1.使用说明 ​ 存储过程是数据库的一个重要的对象,可以封装SQL语句集,可以用来完成一些较复杂的业务逻辑,并且可以入参出参(类似于java中的方法的书写). ...

  7. 项目:Six Sigma

    六西格玛管理(Six Sigma Management)是20世纪80年代末首先在美国摩托罗拉公司发展起来的一种新型管理方式.推行六西格玛管理就是通过设计和监控过程,将可能的失误减少到最低限度,从而使 ...

  8. vue项目|在弹窗中引入uchart图表子组件不显示

    为了解决uchart作为子组件在主组件里引用但不显示的情况,(同样适用于弹窗之中)目前有三种方法. 1-解决方式 1>如果你使用的uchart子组件是从官方拿的例子:进入到uchart子组件将o ...

  9. 【雅礼集训 2017 Day2】棋盘游戏

    loj 6033 description 给一个\(n*m\)的棋盘,'.'为可通行,'#'为障碍.Alice选择一个起始点,Bob先手从该点往四个方向走一步,Alice再走,不能走走过的点,谁不能动 ...

  10. golang 方法接收者

    [定义]: golang的方法(Method)是一个带有receiver的函数Function,Receiver是一个特定的struct类型,当你将函数Function附加到该receiver, 这个 ...