【JUC系列-09】深入理解ReentrantReadWriteLock的底层实现

news2025/1/15 19:44:17

JUC系列整体栏目


内容链接地址
【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429
【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786
【三】熟练掌握Atomic原子系列基本使用https://blog.csdn.net/zhenghuishengq/article/details/132543379
【四】精通Synchronized底层的实现原理https://blog.csdn.net/zhenghuishengq/article/details/132740980
【五】通过源码分析AQS和ReentrantLock的底层原理https://blog.csdn.net/zhenghuishengq/article/details/132857564
【六】深入理解Semaphore底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132908068
【七】深入理解CountDownLatch底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/133343440
【八】深入理解CyclicBarrier底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/133378623
【九】深入理解ReentrantReadWriteLock 读写锁的底层实现https://blog.csdn.net/zhenghuishengq/article/details/133629550

深入理解ReentrantReadWriteLock 读写锁的底层实现

  • 一,读写锁
    • 1,读写锁的基本使用
    • 2,读写锁的底层实现原理
      • 2.1,读写状态state的设计
      • 2.2,写锁的底层实现
        • 2.2.1,lock加锁逻辑
        • 2.2.2,unlock解锁逻辑
      • 2.3,读锁的底层实现
        • 2.3.1,读锁的加锁逻辑
        • 2.3.1,读锁的解锁逻辑
    • 3,总结

一,读写锁

前面几篇讲解了AQS的一些具体的实现,在对共享资源进行操作时,要么就是只能使用独占锁,对每一个线程都进行一个加锁的操作,要么就是单独使用共享锁,同时存在对多个资源的操作,但是在遇到类似缓存这种读多写少,读写同时存在的时候,那么就需要考虑独占锁和共享锁同时存在的需求了。

在AQS的具体实现类中,就存在同时对共享资源读和写的操作的实现类,就是并发包中提供的ReentrantReadWriteLock 类,在该类内部,就实现了读锁和一个写锁。总而言之就是在多个线程只需要读取数据的时候,可以使用共享结点实现,如果存在一个线程需要去写数据的时候,那么就需要通过独占结点实现,用一句话概况就是:读读共享、写写互斥

public class ReentrantReadWriteLock implements ReadWriteLock{
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
}

1,读写锁的基本使用

在深入地了解这个类的底层原理之前,先了解一下这个类的基本使用,在这个 ReentrantReadWriteLock 类的注释中,已经有一个实例来方便我们了解这个类的基本使用,不得不说AQS里面所有的类的注释是非常的详细的

在这里插入图片描述

接下来参考他给的注释,仿写一个用例,用hashMap作为一级缓存,实现数据的缓存和读取。首先先定义一个线程池的工具类

/**
 * 线程池工具
 * @author zhenghuisheng
 * @date : 2023/10/06
 */
public class ThreadPoolUtil {
    /**
     * io密集型:最大核心线程数为2N,可以给cpu更好的轮换,
     *           核心线程数不超过2N即可,可以适当留点空间
     * cpu密集型:最大核心线程数为N或者N+1,N可以充分利用cpu资源,N加1是为了防止缺页造成cpu空闲,
     *           核心线程数不超过N+1即可
     * 使用线程池的时机:1,单个任务处理时间比较短 2,需要处理的任务数量很大
     * 参考:https://blog.csdn.net/yuyan_jia/article/details/120298564
     */
    public static synchronized ThreadPoolExecutor getThreadPool() {
        if (pool == null) {
            //获取当前机器的cpu
            int cpuNum = Runtime.getRuntime().availableProcessors();
            log.info("当前机器的cpu的个数为:" + cpuNum);
            int maximumPoolSize = cpuNum * 2 ;
            pool = new ThreadPoolExecutor(
                    maximumPoolSize - 2,
                    maximumPoolSize,
                    5L,   //5s
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(),  //数组有界队列
                    Executors.defaultThreadFactory(), //默认的线程工厂
                    new ThreadPoolExecutor.AbortPolicy());  //直接抛异常,默认异常
        }
        return pool;
    }
}

接下来创建一个 PutHashMapTask 线程的任务类,用于添加数据,实现一个callAble接口,可以将添加后的hashMap给返回

/**
 * @Author: zhenghuisheng
 * @Date: 2023/10/6 22:41
 */
@Data
public class PutHashMapTask implements Callable {

    Lock writeLock;
    Map<String,Object> hashMap;

    public PutHashMapTask(Lock writeLock,Map<String,Object> hashMap){
        this.writeLock = writeLock;
        this.hashMap = hashMap;
    }

    @Override
    public Object call() throws Exception {
        //上锁
        writeLock.lock();
        for (int i = 0; i < 1000; i++) {
            hashMap.put(i+"",i);
        }
        //解锁
        writeLock.unlock();
        return hashMap;
    }
}

随后创建一个 GetHashMapTask 的任务类,用于获取数据,不需要返回,直接打印出即可

/**
 * @Author: zhenghuisheng
 * @Date: 2023/10/6 22:41
 */
@Data
public class GetHashMapTask implements Runnable {
    Lock readLock;
    Map<String,Object> hashMap;

    public GetHashMapTask(Lock readLock, Map<String,Object> hashMap){
        this.readLock = readLock;
        this.hashMap = hashMap;
    }

    @Override
    public void run() {
        //上锁
        readLock.lock();
        System.out.println(hashMap.get(new Random(1000)));
        //解锁
        readLock.unlock();
    }
}

随后创建一个主线程类,内部定义一把 ReentrantReadWriteLock 读写锁,然后创建线程池一级创建一个hashMap作为一级缓存,随后读写都操作这个hash桶

/**
 * @Author: zhenghuisheng
 * @Date: 2023/10/6 22:36
 */
public class ReentrantReadWriteLockDemo {
    //定义一把读写锁
    private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    //初始化一个线程池
    static ThreadPoolExecutor threadPool = ThreadPoolUtil.getThreadPool();
    //创建一个hashMap作为一级缓存
    static Map<String,Object> hashMap = new HashMap<>();

    public static void main(String[] args) throws Exception {
        //写入数据任务线程
        PutHashMapTask putHashMapTask = new PutHashMapTask(readLock,hashMap);
        //向线程池中添加任务
        Future<?> submit = threadPool.submit(putHashMapTask);
        hashMap =  (Map<String,Object>)submit.get();
        System.out.println(hashMap.size());
        //读取数据任务线程
        GetHashMapTask getHashMapTask = new GetHashMapTask(writeLock,hashMap);
        threadPool.execute(getHashMapTask);
        //主线程休眠
        Thread.sleep(100);
        System.exit(0);
    }
}

上面只是讲解了这个基本使用,还有一些读读共享,写写互斥以及写锁降级成读锁在这个类中都有详细的注释。

在写锁降级成读锁时,会在写锁释放锁之前,给读锁进行一个加锁的操作,主要是为了解决在高并发的场景下,在修改状态时为了保证所有线程的可见性问题,因为在工作线程将变量刷新到主内存时,会有一定的延迟,这样做得好处就是提前将值刷回到缓存,从而解决可见性的延迟问题。总而言之,写锁到读锁降级是为了保证可见性的问题,通过提前刷盘,保证缓存一致性,防止来不及刷盘而读取到脏数据

{	
	rwl.readLock().lock();
	if (!cacheValid) {
     	rwl.readLock().unlock();
     	rwl.writeLock().lock();
     	try {
            //读锁在写锁释放锁之前先加锁
       		rwl.readLock().lock();
        }
     } finally {
       rwl.writeLock().unlock();
     }
}    

在这个读写锁中,目前还是只支持锁的降级,并没有支持锁的升级的,主要还是因为这个线程可见性的问题,就是假设在同一时刻多个线程来读取数据,其中一个线程修改了数据,那么此时读取线程的数据就变成了脏数据,这就是为啥有延迟双删这个实现的原因,尤其是在缓存中,网络问题等等不可避免的因素,以及最主要的可见性的问题,因此在读写锁中,并没有这个读锁到写锁升级的过程

2,读写锁的底层实现原理

在看底层源码之前,先看这个ReentrantReadWriteLock的类图,如下图所示,其顶层接口是一个readWriteLock,内部定义读锁和写锁的的抽象方法,内部同时引入了Sync,该接口是一个AQS的子类,具有AQS的所有特性,一次内部具有公平锁和非公平锁,可重入等特性

请添加图片描述

而不管是读锁还是写锁,内部都实现了这个Lock的接口,Lock内部定义了所有的加解锁的规范,整个AQS中,Lock是作为一个全局的规范类来使用的

public static class WriteLock implements Lock{}
public static class ReadLock implements Lock{}

2.1,读写状态state的设计

在之前的AQS系列中,都是通过cas + 同步等待队列来实现这个加锁的方式,不管是共享锁还是独占锁,同步状态器state的值都是只用于表示独占状态或者共享状态,就是一个单一的职责,那么读写锁是如何通过这个state来表示两种状态的呢,直接看源码吧

在这个ReentrantReadWriteLock的内部类中,有一个Sync的抽象的静态内部类,在中间有一段注释,其翻译结果如下

读取与写入计数提取常量和函数。锁状态在逻辑上分为两个无符号短路:较低的一个表示独占(写入器)锁保持计数,而较高的是共享(读取器)锁保持数

请添加图片描述

也就是说,读写锁也是通过这个state这个关键字来区分的,state是int的数据类型,占4个字节,就是32位,那么高16位表示的就是共享节点的数据,低16位表示的就是独占节点的数据

//高16位,共享节点	  低16位,独占节点	
00000000 00000000 00000000 00000000

因此就定义了变量 SHARED_SHIFT ,值为16,SHARED_UNIT 表示的是共享节点的单位,1右移16位;MAX_COUNT 表示的是最大线程的数据,不管是共享节点还是独占节点的最大值;EXCLUSIVE_MASK 表示的是独占节点的数据

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

而且在获取数据时,内部已经定义好了返回值,如获取共享结点的线程数,独占结点的重入数等

//获取多少个线程占有这把共享锁,通过右移的方式获取
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
//获取独占锁的总线程数,通过位运算
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

在共享锁中,通过 HoldCounter 方法和 ThreadLocalHoldCounter 来作为计数器的,内部主要是通过ThreadLocal 变量来记录可重入锁的次数。

static final class HoldCounter {
	int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
	}
}

2.2,写锁的底层实现

研究完这个state的底层设计之后,再来研究这个 writeLock 写锁底层的实现原理

private static Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
writeLock.unLock();
2.2.1,lock加锁逻辑

首先依旧是通过这个lock方法作为入口,进去会进入这个 acquire 方法,首先会进入这个 tryAcquire 方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&	//尝试加锁
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))	//加锁失败则进入队列,独占节点
        selfInterrupt();
}

随后查看这个方法,根据方法名称可知这是一个获取锁的方法。

由于是写锁加锁的逻辑,首先会获取state值,如果是0,那么高位肯定是没有值得,那么肯定是不存在读锁,后续直接操作这个写锁的逻辑即可。

如果state值不为0,那么就需要判断这个state是读锁还是写锁,首先会获取 exclusiveCount 写锁的总个数,如果个数为0,那么表示这个state的值都是读锁的,那么直接返回false,如果这个线程不是重入的,那么也会直接返回false;如果当前线程满足上面的任意条件,那么会判断当前线程重入的次数,不能超过2的32次方-1。总而言之可以总结成两句话:写写互斥,写读互斥

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();	//获取当前线程 
    int c = getState();		//获取状态,state位32位的值
    int w = exclusiveCount(c);	//获取独占的总线程个数
    //如果state值不为0
    if (c != 0) {	
        //如果独占的写锁个数为0并且不是重入锁,那么整个状态为读锁状态
        if (w == 0	//写读互斥 
            || current != getExclusiveOwnerThread())	//写写互斥
            return false;	//则直接返回false
        //重入,个数不能大于2的16-1次方
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //state的值+1,因为是写锁,只需要在低位加即可,不需要考虑左移
        setState(c + acquires);
        return true;
    }
	//如果获取的state值为0,那么高16位肯定是全为0,那么就进入独占锁逻辑 
    if (writerShouldBlock() ||	//阻塞操作,没有队列则创建
            !compareAndSetState(c, c + acquires))	//cas抢锁
        return false;
    //同步状态器设置当前线程为独占锁线程
    setExclusiveOwnerThread(current);
    return true;
}

如果获取锁失败,则会执行入队的操作,前面几篇AQS的文章在写入队,阻塞的这些方法已经写了很多次了,因此这里不做详细的解释

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

上面的是入队的操作,下面的是阻塞的逻辑,可以查看前面几篇AQS的文章,写的很详细了

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
2.2.2,unlock解锁逻辑

上面讲解了加锁的逻辑,下面的是writeLock解锁的逻辑,比较简单

public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
}

首先会进入 tryRelease 方法中释放锁,就是将state的状态减1,由于是写锁,因此对低位操作刚好就是减少写锁的状态值

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;	//状态减1
    boolean free = exclusiveCount(nextc) == 0;	//判断写锁状态是否为0
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

最后会调用这个 unparkSuccessor 进行出队和唤醒的操作,最终是调用这个LockSupport.unpark方法

LockSupport.unpark(s.thread);

2.3,读锁的底层实现

写锁的逻辑相对而言是比较简单的,接下来查看读锁的底层逻辑实现

private static Lock readLock = readWriteLock.readLock();
readLock.lock();
readLock.unLock();
2.3.1,读锁的加锁逻辑

依旧先查看这个readLock的lock加锁方法,很明显,操作的结点是一个共享结点,上面的写锁是一个独占结点。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

继续查看内部tryAcquireShared方法的加锁逻辑,很明显代码比写锁的更多。

首先依旧是会先判断是否存在写结点,如果存在直接返回-1,并且会判断在写结点降级成读结点时,结点是否为同一个结点,如果不是同一个结点则返回-1

如果只存在读结点,则会进行尝试加锁的操作,如果是第一次加这个读锁,则会单独记录,如果不是第一次加,则会将这些可重入锁存储在ThreadLocal里面,从而记录可重入锁的总数。最后如果加锁失败,则会自旋重试加锁

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();		//获取同步状态器的值
    if (exclusiveCount(c) != 0 &&	//低位写结点的值不为0,读写互斥
            getExclusiveOwnerThread() != current)	//判断当前线程是否写线程,降级操作
        return -1;
    int r = sharedCount(c);		//读锁的总数
    if (!readerShouldBlock() &&	
            r < MAX_COUNT &&	//小于65535
            compareAndSetState(c, c + SHARED_UNIT)) {	//尝试加锁,加65535
        if (r == 0) {	//第一次进来获取读锁
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {	//代表重入
            firstReaderHoldCount++;	
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh); 
            rh.count++; //threadLocl + 1
        }
        return 1;
    }
    return fullTryAcquireShared(current);	//	加锁失败再次尝试获取
}
2.3.1,读锁的解锁逻辑

接下来继续查看这个unlock方法,首先会进入这个 releaseShared 方法

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

接下来查看这个 tryReleaseShared 释放锁的方法,其内部主要就是做了两件事,第一件就是将重入锁的值降低为0,第二件事就是讲高位的值进行减1的操作

protected final boolean tryReleaseShared(int unused) {   
	Thread current = Thread.currentThread();
    //如果有可重入锁,则清0
    if (firstReader == current) {	
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1) firstReader = null;
        else firstReaderHoldCount--;
    } else {
        ReentrantReadWriteLock.Sync.HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0) throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;	//减高位的值
        if (compareAndSetState(c, nextc)) return nextc == 0;
    }
}

3,总结

ReentrantReadWriteLock读写锁底层也是通过AQS实现,通过state的高低位来区分读锁和写锁,高位表示读锁,低位表示写锁,高位读锁采用的是共享结点,低位写锁采用的是独占节点,共享结点中采用ThreadLocal来保存重入锁的次数,在整个读写锁中,主要是满足以下的规则:读读共享、读写互斥、写写互斥、写读降级

内部的ReadLock和WriteLock满足AQS的所有特性,内部也全部实现了CAS抢锁、失败入队、队列不存在则先创建队列,线程阻塞、修改结点状态、结点出队、线程唤醒等操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1065225.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈智能安全配电装置在老年人建筑中的应用

摘要&#xff1a;我国每年因触电伤亡人数非常多&#xff0c;大多数事故是发生在用电设备和配电装置。在电气事故中&#xff0c;无法预料和不可抗拒的事故是比较少的&#xff0c;大量用电事故可采取切实可行措施来预防。本文通过结合老年人建筑的特点和智能安全配电装置的功能&a…

教你三步搞定VsCode调试C++

目录 1 配置编译任务2 配置调试任务3 进行调试 1 配置编译任务 使用VsCode进行C开发时&#xff0c;除了在机器上安装必要的编译工具&#xff08;例如&#xff0c;gcc、g、cmake等&#xff09;之外&#xff0c;还需要在VsCode配置编译任务&#xff0c;从而可以通过点击或者快捷…

【MySql】mysql之进阶查询语句

目录 一、常用查询 1、order by按关键字排序❤ 1.1 升序排序 1.2 降序排序 1.3 结合where进项条件过滤再排序 1.4 多字段排序 2、and和or判断 2.1 and和or的使用 2.2 嵌套、多条件使用 3、distinct 查询不重复记录 4、group by 对结果进行分组 5、limit限制结果…

MySQL57部署与配置[Windows10]

下载原始安装包 https://dev.mysql.com/downloads/installer/https://downloads.mysql.com/archives/notifier/默认安装 MySQL57 默认安装 MySQL Notifier 环境变量配置 Path: C:\Program Files\MySQL\MySQL Server 5.7\binDBeaver数据库连接

【MySql】4- 实践篇(二)

文章目录 1. SQL 语句为什么变“慢”了1.1 什么情况会引发数据库的 flush 过程呢&#xff1f;1.2 四种情况性能分析1.3 InnoDB 刷脏页的控制策略 2. 数据库表的空间回收2.1 innodb_file_per_table参数2.2 数据删除流程2.3 重建表2.4 Online 和 inplace 3. count(*) 语句怎样实现…

websocket拦截

python实现websocket拦截 前言一、拦截的优缺点优点缺点二、实现方法1.环境配置2.代码三、总结现在的直播间都是走的websocket通信,想要获取websocket通信的内容就需要使用websocket拦截,大多数是使用中间人代理进行拦截,这里将会使用更简单的方式进行拦截。 前言 开发者工…

RK3568平台开发系列讲解(外设篇)AP3216C 三合一环境传感器驱动

🚀返回专栏总目录 文章目录 一、AP3216C 简介二、AP3216C驱动程序2.1、设备树修改2.2、驱动程序沉淀、分享、成长,让自己和他人都能有所收获!😄 📢在本篇将介绍AP3216C 三合一环境传感器的驱动。 一、AP3216C 简介 AP3216C 是由敦南科技推出的一款传感器,其支持环境光…

OpenWrt使用Privoxy插件修改UA

OpenWrt使用privoxy修改UA 1.安装privoxy插件 SSH连接到路由器 更新插件列表 update opkg安装插件 opkg install privoxy luci-app-privoxy luci-i18n-privoxy-zh-cn重启路由器 2.配置privoxy 打开配置页面 文件和目录 访问和控制 转发 杂项 日志 编辑配置 浏览器打开 …

Kaggle - LLM Science Exam(一):赛事概述、数据收集、BERT Baseline

文章目录 一、赛事概述1.1 OpenBookQA Dataset1.2 比赛背景1.3 评估方法和代码要求1.4 比赛数据集1.5 优秀notebook 二、BERT Baseline2.1 数据预处理2.2 定义data_collator2.3 加载模型&#xff0c;配置trainer并训练2.4 预测结果并提交2.5 deberta-v3-large 1k Wiki&#xff…

深入理解Linux网络笔记(三):内核和用户进程协作之epoll

本文为《深入理解Linux网络》学习笔记&#xff0c;使用的Linux源码版本是3.10&#xff0c;网卡驱动默认采用的都是Intel的igb网卡驱动 Linux源码在线阅读&#xff1a;https://elixir.bootlin.com/linux/v3.10/source 2、内核是如何与用户进程协作的&#xff08;二&#xff09; …

Godot 官方2D游戏笔记(1):导入动画资源和添加节点

前言 Godot 官方给了我们2D游戏和3D游戏的案例&#xff0c;不过如果是独立开发者只用考虑2D游戏就可以了&#xff0c;因为2D游戏纯粹&#xff0c;我们只需要关注游戏的玩法即可。2D游戏的美术素材简单&#xff0c;交互逻辑简单&#xff0c;我们可以把更多的时间放在游戏的玩法…

苍穹外卖

1、基础知识扫盲 项目从0到1 需求分析->设计->编码->测试->上线运维 角色 项目经理&#xff1a;对整个项目负责&#xff0c;任务分配&#xff0c;把控进度 产品经理&#xff1a;进行需求调研&#xff0c;输出需求调研文档&#xff0c;产品原型 UI设计师&…

【java计算机毕设】 留守儿童爱心捐赠管理系统 springboot vue html mysql 送文档ppt

1.项目视频演示 【java计算机毕设】留守儿童爱心捐赠管理系统 springboot vue html mysql 送文档ppt 2.项目功能截图 3.项目简介 后端&#xff1a;springboot&#xff0c;前端&#xff1a;vue&#xff0c;html&#xff0c;数据库&#xff1a;mysql&#xff0c;开发软件idea 留…

Springboot使用Aop保存接口请求日志到mysql

1、添加aop依赖 <!-- aop日志 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 2、新建接口保存数据库的实体类RequestLog.java package com.example…

volatile关键字使用总结

先说结论 1. volatile关键字可以让编译器层面减少优化&#xff0c;每次使用时必须从内存中取数据&#xff0c;而不是从cpu缓存或寄存器中获取 2. volatile关键字不能完全禁止指令重排&#xff0c;准确地说是两个volatile修饰的变量之间的命令不会进行指令重排 3. 使用volati…

BLE协议栈1-物理层PHY

从应届生开始做ble开发也差不读四个月的时间了&#xff0c;一直在在做上层的应用&#xff0c;对蓝牙协议栈没有过多的时间去了解&#xff0c;对整体的大方向概念一直是模糊的状态&#xff0c;在开发时也因此遇到了许多问题&#xff0c;趁有空去收集了一下资料来完成了本次专栏&…

毕业设计选题之Android基于移动端的线上订餐app外卖点餐安卓系统源码 调试 开题 lw

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

【gcc】RtpTransportControllerSend学习笔记

本文是对大神 webrtc源码分析(8)-拥塞控制(上)-码率预估 的学习笔记。看了啥也没记住,所以跟着看代码先。CongestionControlHandler 在底层网络可用的时候,会触发RtpTransportControllerSend::OnNetworkAvailability()回调,这里会尝试创建CongestionControlHandler创建后即刻…

在VS Code中优雅地编辑csv文件

文章目录 Rainbow csv转表格CSV to Tablecsv2tableCSV to Markdown Table Edit csv 下面这些插件对csv/tsv/psv都有着不错的支持&#xff0c;这几种格式的主要区别是分隔符不同。 功能入口/使用方法Rainbow csv按列赋色右键菜单CSV to Table转为ASCII表格指令CSV to Markdown …

混合网状防火墙的兴起如何彻底改变网络安全

数字环境在不断发展&#xff0c;随之而来的是日益复杂的网络威胁。 从复杂、持续的攻击到对非传统设备的秘密尝试&#xff0c;网络犯罪分子不断完善他们的策略。 除了这些日益严峻的挑战之外&#xff0c;各组织还在努力应对物联网 (IoT)&#xff0c;尽管大量联网设备收集和传…