StampedLock使用及源码分析:号称比读写锁还要快的锁

news2024/12/27 1:08:27

文章目录

  • 一、StampedLock锁概述
    • 1、StampedLock锁简介
    • 2、ReentrantReadWriteLock回顾
    • 3、ReentrantReadWriteLock导致锁饥饿问题
    • 4、锁饥饿问题的缓解
    • 5、StampedLock与ReentrantReadWriteLock的对比
    • 6、StampedLock特点
    • 7、StampedLock的缺点
  • 二、StampedLock的使用
    • 1、StampedLock的三种模式介绍
      • (1)写锁
      • (2)悲观读锁
      • (3)乐观读锁
    • 2、官方案例
    • 3、使用案例
  • 三、源码分析
    • 1、实例化
    • 2、获取锁过程分析
      • (1)ThreadA获取写锁
      • (2)ThreadB获取读锁
      • (3)ThreadC获取读锁
      • (4)ThreadD获取写锁
    • 3、释放锁过程分析
      • (1)ThreadA释放写锁
      • (2)ThreadB和ThreadC释放读锁
    • 4、乐观读锁解析

一、StampedLock锁概述

1、StampedLock锁简介

StampedLock类是在JDK8引入的一把新锁,其是对原有ReentrantReadWriteLock读写锁的增强,增加了一个乐观读模式,内部提供了相关API不仅优化了读锁、写锁的访问,也可以让读锁与写锁间可以互相转换,从而更细粒度的控制并发。

也叫邮戳锁、票据锁。

2、ReentrantReadWriteLock回顾

  • 读写锁适用于读多写少的场景,内部有写锁和读锁。
  • 读锁是一把共享锁,当一个线程持有某一个数据的读锁时,其他线程也可以对这条数据进行读取,但是不能写。
  • 写锁是一把独占锁,一个线程持有某一个数据的写锁时,其他线程是不可以获取到这条数据的写锁和读锁的。
  • 对于锁升级来说,当一个线程在没有释放读锁的情况下,就去申请写锁,是不支持的。
  • 对于锁降级来说,当一个线程在没有释放写锁的情况下,去申请读锁,是支持的。

3、ReentrantReadWriteLock导致锁饥饿问题

在使用读写锁时,还容易出现写线程饥饿的问题。主要是因为读锁和写锁互斥。比方说:当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。

也就是说,ReentrantReadWriteLock一旦读操作比较多的时候,想要获取写锁就变得比较困难了。假如说当前1000个线程,999个读,1个写,有可能999个读线程长时间抢到了锁,那1个写线程就悲剧了,因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。

4、锁饥饿问题的缓解

对于写线程饥饿问题,可以通过公平锁进行一定程度的解决,但是它是以牺牲系统吞吐量为代价的。

new ReentrantReadWriteLock(true);

5、StampedLock与ReentrantReadWriteLock的对比

ReentrantReadWriteLock允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的Synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发,读读可以共享

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验

总之,对短的只读代码段,使用乐观模式通常可以减少争用并提高吞吐量

6、StampedLock特点

获取锁的方法,会返回一个票据(stamp),当该值为0代表获取锁失败,其他值都代表成功。

释放锁的方法,都需要传递获取锁时返回的票据(stamp),这个stamp必须是和成功获取锁时得到的Stamp一致,从而控制是同一把锁。

StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁就会造成死锁。

StampedLock提供了三种模式控制读写操作:写锁、悲观读锁、乐观读锁

在StampedLock中读锁和写锁可以相互转换,而在ReentrantReadWriteLock中,写锁可以降级为读锁,而读锁不能升级为写锁。

7、StampedLock的缺点

  • StampedLock不支持重入。
  • StampedLock的悲观读锁和写锁都不支持条件变量(Condition)。
  • 使用StampedLock一定不要调用中断操作,即不要使用interrupt()方法,会影响性能。

二、StampedLock的使用

1、StampedLock的三种模式介绍

(1)写锁

使用类似于ReentrantReadWriteLock,是一把独占锁,当一个线程获取该锁后,其他请求线程会阻塞等待。 对于一条数据没有线程持有写锁或悲观读锁时,才可以获取到写锁,获取成功后会返回一个票据,当释放写锁时,需要传递获取锁时得到的票据。

(2)悲观读锁

使用类似于ReentrantReadWriteLock,是一把共享锁,多个线程可以同时持有该锁。当一个数据没有线程获取写锁的情况下,多个线程可以同时获取到悲观读锁,当获取到后会返回一个票据,并且阻塞线程获取写锁。当释放锁时,需要传递获取锁时得到的票据。

(3)乐观读锁

这把锁是StampedLock新增加的。可以把它理解为是一个悲观锁的弱化版。当没有线程持有写锁时,可以获取乐观读锁,并且返回一个票据。值得注意的是,它认为在获取到乐观读锁后,数据不会发生修改,获取到乐观读锁后,其并不会阻塞写入的操作。

那这样的话,它是如何保证数据一致性的呢? 乐观读锁在获取票据时,会将需要的数据拷贝一份,在真正读取数据时,会调用StampedLock中的API,验证票据是否有效。如果在获取到票据到使用数据这期间,有线程获取到了写锁并修改数据的话,则票据就会失效。 如果验证票据有效性时,当返回true,代表票据仍有效,数据没有被修改过,则直接读取原有数据。当返回flase,代表票据失效,数据被修改过,则重新拷贝最新数据使用。

乐观读锁适用于一些很短的只读代码,它可以降低线程之间的锁竞争,从而提高系统吞吐量。但对于读锁获取数据结果必须要进行校验。

2、官方案例

public class Point {
    //定义共享数据
    private double x, y;

    //实例化锁
    private final StampedLock sl = new StampedLock();

    //写锁案例
    void move(double deltaX, double deltaY) {

        //获取写锁
        long stamp = sl.writeLock();

        try {
            x += deltaX;
            y += deltaY;
        } finally {
            //释放写锁
            sl.unlockWrite(stamp);
        }
    }

    //使用乐观读锁案例
    double distanceFromOrigin() {

        long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁

        double currentX = x, currentY = y; //将两个字段读入本地局部变量
        if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?

            stamp = sl.readLock(); //如果有,我们再次获得一个读悲观锁
            try {
                currentX = x; // 将两个字段读入本地局部变量
                currentY = y; // 将两个字段读入本地局部变量
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX
                + currentY * currentY);
    }

    //使用悲观读锁并锁升级案例
    void moveIfAtOrigin(double newX, double newY) {

        // 获取悲观读锁
        long stamp = sl.readLock();

        try {
            while (x == 0.0 && y == 0.0) {//循环,检查当前状态是否符合
                //锁升级,将读锁转为写锁
                long ws = sl.tryConvertToWriteLock(stamp);
                //确认转为写锁是否成功
                if (ws != 0L) {
                    stamp = ws; //如果成功 替换票据
                    x = newX; //进行状态改变
                    y = newY; //进行状态改变
                    break;
                } else { //如果不成功
                    sl.unlockRead(stamp); //显式释放读锁
                    stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
                }
            }
        } finally {
            //释放读锁或写锁
            sl.unlock(stamp);
        }
    }
}

3、使用案例

public class StampedLockDemo {

    static int number = 10;
    static StampedLock stampedLock = new StampedLock();

    // 写锁案例
    public void write() {
        // 写锁,获取stamp
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            // 写操作
            number = number + 1;
        } finally {
            // 释放锁
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    // 悲观读锁案例,读的过程不允许写锁介入
    public void read() {
        // 读锁
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程begin");
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取 4s");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "读取结束" + result);
        } finally {
            // 释放读锁
            stampedLock.unlockRead(stamp);
        }

    }

    // 乐观读,读的过程允许写锁介入
    public void read2() {
        // 乐观读
        long stamp = stampedLock.tryOptimisticRead();
        // 乐观的认为,读取中没有线程修改过值
        int result = number;
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取 3s");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //检查发出乐观读锁后同时是否有其他写锁发生,true为无修改,false为有修改
        if(!stampedLock.validate(stamp)) {
            System.out.println(Thread.currentThread().getName() + "\t" + "乐观读失败,数据有变动,升级为悲观读");
            try {
                // 变更锁,升级为悲观锁
                stamp = stampedLock.readLock();
                result = number;
            } finally {
                // 解锁
                stampedLock.unlockRead(stamp);
            }

        }

        System.out.println(Thread.currentThread().getName() + "\t" + "读取的数据" + result);


    }

    public static void main(String[] args) {
        StampedLockDemo stampedLockDemo = new StampedLockDemo();
        // 读线程
        new Thread(() -> {
            stampedLockDemo.read2();
        }, "readThread1").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 写线程
        new Thread(() -> {
            stampedLockDemo.write();
        }, "writeThread").start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 读线程
        new Thread(() -> {
            stampedLockDemo.read2();
        }, "readThread2").start();
    }
}

三、源码分析

1、实例化

StampedLock是基于CLH自旋锁实现,锁会维护一个等待线程链表队列,所有没有成功申请到锁的线程都以FIFO的策略记录到队列中,队列中每个节点代表一个线程,节点保存一个标记位,判断当前线程是否已经释放锁。

当一个线程试图获取锁时,首先取得当前队列的尾部节点作为其前序节点,并判断前序节点是否已经释放锁,如果前序节点没有释放锁,则当前线程还不能执行,进入自旋等待。如果前序节点已经释放锁,则当前线程执行。

首先需要先了解一些StampedLock类的常量值,方便与后面源码的理解。
在这里插入图片描述
另外还有两个很重要的属性:state、readerOverFlow:
state:当前锁的状态,是由写锁占用还是由读锁占用。其中long的倒数第八位是1,则表示由写锁占用(00000001),前七位由读锁占用(1-126)。
readerOverFlow:当读锁的数量超过了范围,通过该值进行记录。

当实例化StampedLock时,会设置节点状态值为ORIGIN(0000 0000)。
在这里插入图片描述

2、获取锁过程分析

假设现在有四个线程:ThreadA获取写锁、ThreadB获取读锁、ThreadC获取读锁、ThreadD获取写锁。

(1)ThreadA获取写锁

该方法用于获取写锁,如果当前读锁和写锁都未被使用的话,则获取成功并更新state,返回一个long值,代表当前写锁的票据,如果获取失败,则调用acquireWrite()将写锁放入等待队列中。

因为当前还没有任务线程获取到锁,所以ThreadA获取写锁成功。

// java.util.concurrent.locks.StampedLock#writeLock
public long writeLock() {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}

(2)ThreadB获取读锁

// java.util.concurrent.locks.StampedLock#readLock
public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}

该方法用于获取读锁,如果写锁未被占用,则获取成功,返回一个long值,并更新state,如果有写锁存在,则调用acquireRead(),将当前线程包装成一个WNODE放入等待队列,线程会被阻塞。

因为现在ThreadA已经获取到了写锁并且没有释放,所以ThreadB在获取读锁时,一定会阻塞,被包装成WNode进入等待队列中。

在acquireRead()内部会进行两次for循环进行自旋尝试获取锁,每次for循环次数由CPU核数决定,进入到该方法后,首先第一次自旋会尝试获取读锁,获取成功,则直接返回。否则,ThreadB会初始化等待队列,并创建一个WNode,作为队头放入等待队列,其内部模式为写模式,线程对象为null,status为0【初始化】。同时还会将当前线程ThreadB包装为WNode放入等待队列的队尾中,其内部模式为读模式,thread为当前ThreadB对象,status为0。

在这里插入图片描述
当进入到第二次自旋后,还是先尝试获取读锁,如果仍没有获取到,则将前驱节点的状态设置为-1【WAITING】,用于代表当前ThreadB已经进入等待阻塞。

在这里插入图片描述

(3)ThreadC获取读锁

ThreadC在获取读锁时,其过程与ThreadB类似,因为ThreadA的写锁没有释放,ThreadC也会进入等待队列。但与ThreadB不同的是,ThreadC不会占用等待队列中的一个新节点,因为其前面的ThreadB也是一个读节点,它会赋值给用于表达ThreadB的WNode中的cowait属性,实际上构成一个栈。
在这里插入图片描述

(4)ThreadD获取写锁

由于ThreadA的写锁仍然没有释放,当ThreadD调用writeLock()获取写锁时,内部会调用acquireWrite()。

acquireWrite()内部的逻辑和acquireRead()类似,也会进行两次自旋。第一次自旋会先尝试获取写锁,获取成功则直接返回,获取失败,则会将当前线程TheadD包装成WNode放入等待队列并移动队尾指针,内部属性模式为写模式,thread为ThreadD对象,status=0【初始化】。
在这里插入图片描述
当进入到第二次自旋,仍然会尝试获取写锁,如果获取不到,会修改其前驱节点状态为-1【等待】,并阻塞当前线程。
在这里插入图片描述

3、释放锁过程分析

(1)ThreadA释放写锁

当要释放写锁时,需要调用unlockWrite(),其内部首先会判断,传入的票据与获取锁时得到的票据是否相同,不同的话,则抛出异常。如果相同先修改state,接着调用release(),唤醒等待队列中的队首节点【即头结点whead的后继节点】

// java.util.concurrent.locks.StampedLock#unlockWrite
public void unlockWrite(long stamp) {
    WNode h;
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    if ((h = whead) != null && h.status != 0)
        release(h);
}
// 唤醒队列的队首节点【头结点whead的后继节点】
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0); // 将头结点状态从-1变为0,标识要唤醒其后继节点
        if ((q = h.next) == null || q.status == CANCELLED) { // 判断头结点的后继节点是否为null或状态为取消
            for (WNode t = wtail; t != null && t != h; t = t.prev) // 从队尾查找距头结点最近的状态为等待的节点
                if (t.status <= 0)
                    q = t; // 赋值
        }
        if (q != null && (w = q.thread) != null)
            U.unpark(w); // 唤醒队首节点
    }
}

在release()中,它会先将头结点whead的状态修改从-1变为0,代表要唤醒其后继节点,接着会判断头结点whead的后继节点是否为null或者其后继节点的状态是否为1【取消】。 如果不是,则直接调用unpark()唤醒队首节点,如果是的话,再从队尾开始查找距离头结点最近的状态<=0【WAITING或初始化】的节点。
在这里插入图片描述
当ThreadB被唤醒后,它会从cowait中唤醒栈中的所有线程,因为读锁是一把共享锁,允许多线程同时占有。

在这里插入图片描述
当所有的读锁都被唤醒后,头结点指针会后移,指向ThreadB这个WNode,并将原有的头结点移出等待队列
在这里插入图片描述
此时ThreadC已经成为了孤立节点,最终会被GC。最终队列结构:
在这里插入图片描述

(2)ThreadB和ThreadC释放读锁

读锁释放需要调用unlockRead(),其内部先判断票据是否正确,接着会对读锁数量进行扣减,当读锁数量为0,会调用release()唤醒队首节点。

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
        if (m < RFULL) {
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L)
            break;
    }
}

其内部同样会先将头结点状态从-1该为0,标识要唤醒后继节点
在这里插入图片描述
当ThreadD被唤醒获取到写锁后,头结点指针会后移指向ThreadD,并原有头部节点移出队列。
在这里插入图片描述

4、乐观读锁解析

在ReentrantReadWriteLock中,只有写锁和读锁的概念,但是在读多写少的环境下,容易出现写线程饥饿问题,虽然能够通过公平锁解决,但会造成系统吞吐量降低。

乐观读锁只需要获取,不需要释放。在获取时,只要没有线程获取写锁,则可以获取到乐观读锁,同时将共享数据储存到局部变量中。同时在获取到乐观读锁后,并不会阻塞其他线程对共享数据进行修改。

因为就会造成当使用共享数据时,出现数据不一致的问题。因为在使用乐观读锁时,要反复的对数据进行校验。

public long tryOptimisticRead() {
    long s; // 没有线程获取写锁,则乐观读锁获取成功,返回票据
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

public boolean validate(long stamp) {
    U.loadFence(); // 传入乐观读锁stamp,验证是否有线程获取到写锁
    return (stamp & SBITS) == (state & SBITS);
}

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

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

相关文章

基于PHP校园疫情防控信息管理系统-计算机毕设 附源码12057

PHP校园疫情防控信息管理系统 摘 要 如今计算机行业的发展极为快速&#xff0c;搭载于计算机软件运行的数据库管理系统在各行各业得到了广泛的运用&#xff0c;其在数据管理方面具有的准确性和高效性为大中小企业的日常运营提供了巨大的帮助。自从2020年新冠疫情爆发以来&…

ArduPilot H743 Dual BMI270 Mark4 四轴DIY简单功能验证

ArduPilot H743 Dual BMI270 Mark4 四轴DIY简单功能验证 1. 源由2. 梳理2.1 基本配置2.2 滤波配置2.3 FPV-VTX配置2.4 FPV操控2.5 自适应PID调参2.6 电传配置 3. 视频3.1 FPV操控性3.2 路点巡航3.3 救援模式 4. 总结5. 参考资料6. 补充说明--问题集中回答 1. 源由 基于Mark4机…

易上手的数据报表工具有哪些?奥威BI零编程

易上手的数据报表功能有哪些&#xff1f;实际上&#xff0c;国产的BI报表工具都算得上是易上手的&#xff0c;因为它们大多都是低代码的BI报表工具&#xff0c;只需掌握基础SQL即可。但奥威BI报表工具却是零编程做大数据分析的BI报表工具。要说易上手&#xff0c;奥威BI报表工具…

Spark-统一内存模型

总结&#xff1a; Spark的内存模型分为4部分&#xff0c;分别是存储内存&#xff0c;计算内存&#xff0c;其他内存&#xff0c;预留内存&#xff1b; 其中存储内存和计算内存可以动态占用&#xff0c;当己方内存不足对方空余则可占用对方的内存&#xff0c;计算内存被存储内…

Attention机制竟有bug,Softmax是罪魁祸首,影响所有Transformer

大模型开发者&#xff0c;你们错了&#xff01; 「我发现注意力公式里有个 bug&#xff0c;八年了都没有人发现。所有 Transformer 模型包括 GPT、LLaMA 都受到了影响。」 昨天&#xff0c;一位名叫 Evan Miller 的统计工程师的话在 AI 领域掀起了轩然大波。 我们知道&#x…

解决单节点es索引yellow

现象 单节点的es&#xff0c;自动创建索引后&#xff0c;默认副本个数为1&#xff0c;索引状态为yellow 临时解决 修改副本个数为0 永久解决 方法1、修改elasticsearch.yml文件&#xff0c;添加配置并重启es number_of_replicas&#xff1a;副本分片数&#xff0c;默认…

UG\NX二次开发 创建分割面、细分面

文章作者:里海 来源网站:https://blog.csdn.net/WangPaiFeiXingYuan 简介: 群友问“UF没有分割面吗” 这个是有的,下面就是分割面(细分面)的演示效果和c++源代码。 效果: 代码: #include "me.hpp" #include <stdio.h> #include <string.h> #i…

财报解读:英特尔二季度业绩回暖,IFS业务增长势能已然释放?

英特尔终于看到了盈利曙光。 北京时间7月28日&#xff0c;英特尔公布了2023财年第二季度财报&#xff0c;数据显示公司业绩超出市场预期&#xff0c;财报一经发布&#xff0c;第二个交易日英特尔股价上涨6.6%&#xff0c;最终报收36.83美元。 &#xff08;图片来源&#xff1a…

挪威网络安全大危机?数据备份刻不容缓

挪威国家安全局近期确认&#xff0c;黑客利用Ivanti Endpoint Manager Mobile&#xff08;EPMM&#xff09;解决方案中的零日漏洞对多个软件平台进行攻击&#xff0c;访问并窃取了受感染系统中的敏感数据&#xff0c;导致出现数据泄露问题。 此次零日漏洞&#xff08;CVE-2023-…

CentOS7环境安装tomcat

环境准备 由于是在练习&#xff0c;为了方便&#xff0c;我们可以 1.关闭防火墙 systemctl disable firewalld.service systemctl stop firewalld.service 2.关闭selinux 在/etc/selinux/config中&#xff0c;设置&#xff1a; SELINUXdisabled 3.准备jdk---》jdk-8u333-li…

通过电商项目,详解抓包到接口测试,附验证码 +cookie 问题处理!

一、背景 通常来说&#xff0c;进行接口测试&#xff0c;开发会提供对应的接口文档给到测试&#xff0c;但也有例外。开发无接口文档&#xff0c;但领导又需要你对刚开发的软件&#xff0c;进行接口测试、接口自动化测试、甚至是性能测试。这个时候作为专业测试应该怎么办&…

WebDAV之π-Disk派盘+Flacbox

Flacbox是一款移动端的文件管理器和播放器应用。它支持在iOS设备上进行文件管理和音频播放,包括FLAC、MP3、AAC等格式的音频文件。Flacbox还支持WebDAV协议,可以通过WebDAV连接来访问和管理WebDAV服务器上的文件。 π-Disk派盘 – 知识管理专家 派盘是一款面向个人和企业的…

CMake:为Eigen库使能向量化

CMake:为Eigen库使能向量化 导言构建Eigen项目结构CMakeLists.txt相关源码 导言 本篇开始将涉及检测外部库相关的内容&#xff0c;期间会穿插着一些其他的内容。为了能够使得系统在系统中运行Eigen库&#xff0c;我们首先需要在系统中配置好Eigen库。然后介绍与Eigen库相关的C…

PostMan调用gitlab接口,OAuth 2.0 身份认证 API ,copy完事~

获取token接口&#xff1a; https://gitlab.***.com/oauth/token &#xff0c;接下来就可以调用其他功能的接口了 例&#xff1a;创建账户&#xff0c;将获取到的access_token放置在接口请求的token中 其他接口调用同上

【雕爷学编程】MicroPython动手做(27)——物联网之掌控板小程序3

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

【图解】反卷积Deconvolution

1. 反池化和反卷积 以下是DeconvNet的整体架构&#xff1a; unpooling&#xff1a; (Unpooling操作需要依据max pooling记录的位置,将每个pooling区域还原回去。在进行max pooling时,我们记录下每个pooling区域的最大激活值所在的位置坐标,在unpooling时就可以依据这些坐标,把激…

express学习笔记4 - 热更新以及express-boom

我们每次改动代码的时候都要重启项目&#xff0c;现在我们给项目添加一个热更新 npm install --save-dev nodemon # or using yarn: yarn add nodemon -D 在package.json添加一行代码 "dev": "nodemon ./bin/www" 重启项目 然后随便做改动&#xff…

将子域名连接到 Shopify 的步骤,也就是把不是www的域名链接到shopify商店,二级域名链接到shopify店铺

将子域名连接到 Shopify 的步骤&#xff1a; 1. Shopify 后台&#xff1a; 首先&#xff0c;检查您的 Shopify 控制面板并验证您使用的是 Shopify 提供的免费域&#xff08;也就是xxxxxxxxx.myshopify.com&#xff09;。这是至关重要的&#xff0c;因为它将帮助您在连接子域时避…

数据结构--图的遍历 DFS

数据结构–图的遍历 DFS 树的深度优先遍历 //树的先根遍历 void PreOrder(TreeNode *R) {if(R ! NULL){visit(R); //访问根节点while(R还有下一个子树T)PreOrder(T);//先根遍历下一棵子树} }图的深度优先遍历 bool visited [MAX_VERTEX_NUM]; //访问标记数组 void DFS(Grap…

最长公共前缀_力扣14

文章目录 题目描述法一 横向比较 题目描述 法一 横向比较 将多个字符串比较转换为两两比较&#xff0c;如果比较完最长公共前缀已经是空串&#xff0c;则最长公共前缀一定是空串&#xff0c;因此不需要继续遍历剩下的字符串&#xff0c;直接返回空串。 string longestCommonPre…