【并发编程学习篇】ReentrantLock设计思想剖析

news2024/12/19 0:08:14

一、AQS原理剖析

什么是AQS

  1. java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如 等待队列条件队列独占获取共享获取
  2. 而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的 同步器
  3. JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的
  4. 一般是通过一个内部类Sync继承 AQS,然后将同步器所有调用都映射到Sync对应的方法

AQS具备的特性: 阻塞等待队列、共享/独占、公平/非公平、可重入、允许中断

AQS内部维护属性 “volatile int state” 表示资源的可用状态

State三种访问方式: getState() 、setState() 、compareAndSetState()

AQS定义两种资源共享方式

  1. Exclusive-独占,只有一个线程能执行,如ReentrantLock
  2. Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

AQS定义两种队列

  1. 同步等待队列: 主要用于维护获取锁失败时入队的线程

  2. 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁

AQS 定义了5个队列中节点状态:

  1. 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。

  2. CANCELLED,值为1,表示当前的线程被取消;

  3. SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;

  4. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;

  5. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

自定义同步器实现时主要实现以下几种方法:

  1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

同步等待队列

AQS当中的同步等待队列也称 CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是 FIFO 先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。

AQS 依赖CLH同步队列来完成同步状态的管理:

  1. 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程

  2. 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

  3. 通过 signalsignalAll 将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)

在这里插入图片描述

条件等待队列

AQS中条件队列是使用 单向列表 保存的,用nextWaiter来连接:

调用await方法阻塞线程,当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

Condition接口详解
在这里插入图片描述

  1. 调用Condition#await方法会 释放当前持有的锁,然后阻塞当前线程,同时向Condition队列 尾部 添加一个节点,所以调用Condition#await方法的时候必须持有锁。

  2. 调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部

  3. 然后唤醒因调condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。

等待唤醒机制之await/signal测试

@Slf4j
public class ConditionTest {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug(Thread.currentThread().getName() + " 开始处理任务");
                condition.await();
                log.debug(Thread.currentThread().getName() + " 结束处理任务");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug(Thread.currentThread().getName() + " 开始处理任务");

                Thread.sleep(2000);
                condition.signal();
                log.debug(Thread.currentThread().getName() + " 结束处理任务");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
    }

二、ReentrantLock介绍

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

相对于 synchronized, ReentrantLock具备如下特点:

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
  5. 与 synchronized 一样,都支持可重入

在这里插入图片描述

三、ReentrantLock使用

3.1 线程安全测试

public class ReentrantLockDemo {

    private static  int sum = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(()->{
                //加锁
                lock.lock();
                try {
                    for (int j = 0; j < 10000; j++) {
                        sum++;
                    }
                } finally {
                    // 解锁
                    lock.unlock();
                }
            });
            thread.start();
        }
        Thread.sleep(2000);
        System.out.println(sum);
    }

3.2 可重入

@Slf4j
public class ReentrantLockDemo2 {

    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }


    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }
    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }

3.3 可中断

@Slf4j
public class ReentrantLockDemo3 {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {

            log.debug("t1启动...");

            try {
                lock.lockInterruptibly();
                try {
                    log.debug("t1获得了锁");
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("t1等锁的过程中被中断");
            }

        }, "t1");

        lock.lock();
        try {
            log.debug("main线程获得了锁");
            t1.start();
            //先让线程t1执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.interrupt();
            log.debug("线程t1执行中断");
        } finally {
            lock.unlock();
        }
    }

3.4 锁超时

立即失败

@Slf4j
public class ReentrantLockDemo4 {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {

            log.debug("t1启动...");
            // 注意: 即使是设置的公平锁,此方法也会立即返回获取锁成功或失败,公平策略不生效
            if (!lock.tryLock()) {
                log.debug("t1获取锁失败,立即返回false");
                return;
            }
            try {
                log.debug("t1获得了锁");
            } finally {
                lock.unlock();
            }

        }, "t1");


        lock.lock();
        try {
            log.debug("main线程获得了锁");
            t1.start();
            //先让线程t1执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

超时失败

@Slf4j
public class ReentrantLockDemo4 {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            log.debug("t1启动...");
            //超时
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("等待 1s 后获取锁失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
            try {
                log.debug("t1获得了锁");
            } finally {
                lock.unlock();
            }

        }, "t1");


        lock.lock();
        try {
            log.debug("main线程获得了锁");
            t1.start();
            //先让线程t1执行
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

3.5 公平锁

ReentrantLock 默认是不公平的

@Slf4j
public class ReentrantLockDemo5 {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock(true); //公平锁 

        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s 之后去争抢锁
        Thread.sleep(1000);

        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    log.debug(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "强行插入" + i).start();
        }
    }
}

输出结果:

t0 running...
t1 running...
t2 running...
......
强行插入0 running...
强行插入1 running...
强行插入2 running...
......

3.6 非公平锁

在这里插入图片描述

四、ReentrantLock源码剖析

4.1 lock方法

image.png

公平&非公平的方法源码

// 公平锁的sync的lock方法
final void lock() {
    acquire(1);
}

// 非公平锁的sync的lock方法
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

4.2 acquire方法

acquire是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法

// 核心acquire     arg = 1
public final void acquire(int arg) {
	/**
	* 1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法,否则执行&&后面的方法
	* 
	* 2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,并且插入到AQS的队列的末尾,并且作为tail
	* 
	* 3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源,否则尝试将线程挂起,阻塞起来!
	*/
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

4.3 tryAcquire方法

tryAcquire分为公平和非公平两种

tryAcquire主要做了两件事:

  • 如果state为0,尝试获取锁资源
  • 如果state不为0,看一下是不是锁重入操作,如果不是

非公平:

// 非公平锁实现!
final boolean nonfairTryAcquire(int acquires) {
    // 拿到当前线程!
    final Thread current = Thread.currentThread();
    // 拿到AQS的state
    int c = getState();
    // 如果state == 0,说明没有线程占用着当前的锁资源
    if (c == 0) {
        // 没人占用锁资源,我直接抢一波(不管有没有线程在排队)
        if (compareAndSetState(0, acquires)) {
            // 将当前占用这个互斥锁的线程属性设置为当前线程
            setExclusiveOwnerThread(current);
            // 返回true,拿锁成功
            return true;
        }
    }
    // 当前state != 0,说明有线程占用着锁资源
    // 判断拿着锁的线程是不是当前线程(锁重入)
    else if (current == getExclusiveOwnerThread()) {
        // 将state再次+1
        int nextc = c + acquires;
        // 锁重入是否超过最大限制
        // 0(符号位) 1111111 11111111 11111111 11111111   + 1
        // 1(符号位)0000000 00000000 00000000 00000000
        // 抛出error
        if (nextc < 0) 
            throw new Error("Maximum lock count exceeded");
        // 将值设置给state
        setState(nextc);
        // 返回true,拿锁成功
        return true;
    }
    return false;
}

公平锁:

// 公平锁实现
protected final boolean tryAcquire(int acquires) {
    // 拿到当前线程!
    final Thread current = Thread.currentThread();
    // 拿到AQS的state
    int c = getState();
    // c == 0 没有其他线程获取锁
    if (c == 0) {
        // 判断是否有线程在排队,如果有线程排队,直接不执行返回最外层的false
        // 如果没有线程排队,执行compareAndSetState()方法,CAS尝试修改status属性为1(获取锁资源)
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current); // 设置当前独占锁的线程
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

4.4 addWaiter方法

在获取锁资源 失败 后,需要将当前线程封装为Node对象,并且插入到AQS队列的末尾

// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {
    // 将当前线程封装为Node对象,mode为null,代表互斥锁
    Node node = new Node(Thread.currentThread(), mode);
    // pred是tail节点
    Node pred = tail;
    // 如果pred不为null,有线程正在排队
    if (pred != null) {
        // 将当前节点的prev,指定tail尾节点
        node.prev = pred;
        // 以CAS的方式,将当前节点变为tail节点
        if (compareAndSetTail(pred, node)) {
            // 之前的tail的next指向当前节点
            pred.next = node;
            return node;
        }
    }
    // 添加的流程为,  自己prev指向、tail指向自己、前节点next指向我
    // 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列
    enq(node);
    return node;
}

// enq,无论怎样都添加进入
private Node enq(final Node node) {
    for (;;) {
        // 拿到tail
        Node t = tail;
        // 如果tail为null,说明当前没有Node在队列中
        if (t == null) { 
            // 创建一个新的Node作为head,并且将tail和head指向一个Node
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 和上述代码一致!
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

4.5 acquireQueued

  1. acquireQueued方法会查看当前排队的Node是否是head的next
  2. 如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
  3. 在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,
  • 如果为1,代表是取消的节点,不能挂起

  • 如果为-1,代表挂起当前线程

  • 如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程

// acquireQueued方法
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {
    // 标识。
    boolean failed = true;
    try {
        // 循环走起
        for (;;) {
            // 拿到上一个节点
            final Node p = node.predecessor();
            if (p == head && // 说明当前节点是head的next
                tryAcquire(arg)) { // 竞争锁资源,成功:true,失败:false
                // 进来说明拿到锁资源成功
                // 将当前节点置位head,thread和prev属性置位null
                setHead(node);
                // 帮助快速GC
                p.next = null; 
                // 设置获取锁资源成功
                failed = false;
                // 不管线程中断。
                return interrupted;
            }
            // 如果不是或者获取锁资源失败,尝试将线程挂起
            // 第一个事情,当前节点的上一个节点的状态正常!
            // 第二个事情,挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
				// 通过LockSupport将当前线程挂起
                parkAndCheckInterrupt())
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 确保上一个节点状态是正确的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 拿到上一个节点的状态
    int ws = pred.waitStatus;
    // 如果上一个节点为 -1
    if (ws == Node.SIGNAL)
        // 返回true,挂起线程
        return true;
    // 如果上一个节点是取消状态
    if (ws > 0) {
        // 循环往前找,找到一个状态小于等于0的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 将小于等于0的节点状态该为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

4.6 unlock方法

释放锁资源:

  • 将state-1。
  • 如果state减为0了,唤醒在队列中排队的Node。(一定唤醒离head最近的)

释放锁不分公平和非公平,就一个方法。

// 真正释放锁资源的方法
public final boolean release(int arg) {
    // 核心的释放锁资源方法
    if (tryRelease(arg)) {
        // 释放锁资源释放干净了。  (state == 0)
        Node h = head;
        // 如果头节点不为null,并且头节点的状态不为0,唤醒排队的线程
        if (h != null && h.waitStatus != 0)// 唤醒线程
            unparkSuccessor(h);
        return true;
    }
    // 释放锁成功,但是state != 0
    return false;
}

// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {
    // 获取state - 1
    int c = getState() - releases;
    // 如果释放锁的线程不是占用锁的线程,抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否成功的将锁资源释放利索 (state == 0)
    boolean free = false;
    if (c == 0) {
        // 锁资源释放干净。
        free = true;
        // 将占用锁资源的属性设置为null
        setExclusiveOwnerThread(null);
    }
    // 将state赋值
    setState(c);
    // 返回true,代表释放干净了
    return free;
}

// 唤醒节点
private void unparkSuccessor(Node node) {
    // 拿到头节点状态
    int ws = node.waitStatus;
    // 如果头节点状态小于0,换为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 拿到当前节点的next
    Node s = node.next;
    // 如果s == null ,或者s的状态为1
    if (s == null || s.waitStatus > 0) {
        // next节点不需要唤醒,需要唤醒next的next
        s = null;
        // 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 经过循环的获取,如果拿到状态正常的节点,并且不为null
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

4.7 为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?

在这里插入图片描述

  1. 因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是通过CAS将tail赋值给当前Node
  2. 最后才是能上一个节点的next指针,指向当前Node。
  3. 如果从前往后,通过next去找,可能CAS成功把tail指向新插入的node时,还未把前驱I节点的next指向当前节点,可能会 丢失 某个节点,导致这个节点不会被唤醒~
  4. 如果从后往前找,肯定可以找到全部的节点。

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

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

相关文章

【python】函数详解

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录基本函数-function模块的引用模块搜索路径不定长参数参数传递传递元组传递字典缺陷&#xff0c;容易改了原始数据&#xff0c;可以用copy()方法避免变量作用域全局变量闭包closurenonlocal 用了这个声明闭包…

C语言基础相关内容

文章目录前言1. 关键字2. C语言数据类型3. 标志符4. 常量类型5. 内存模型变量内存分析数组6. printf & scanfpuchar&getchar7 main函数8 字面值常量前言 本文简明扼要的介绍了部分C语言的一些基本内容。 1. 关键字 12345678charshortintlongfloatdoubleifelsereturnd…

【人脸识别】CurricularFace:自适应课程学习人脸识别损失函数

论文题目&#xff1a;《CurricularFace: Adaptive Curriculum Learning Loss for Deep Face Recognition》 论文地址&#xff1a;https://arxiv.org/pdf/2004.00288v1.pdf 代码地址&#xff1a;https://github.com/HuangYG123/CurricularFace 建议先了解下这篇文章&#xff1a…

电子技术——频率补偿

电子技术——频率补偿 在本节我们介绍修改三极点或多极点放大器的开环增益函数 A(s)A(s)A(s) 的方法&#xff0c;使得闭环增益在我们希望的值上放大器是稳定的。这个过程称为频率补偿。 理论 最简单的频率补偿方法是引入新的极点&#xff0c;如图下面是一个放大器的伯德图&am…

windows安装Ubuntu子系统以及图形化界面记录

文章目录1. windows环境设置2. 开始安装3. ubuntu使用3.1 启动和退出 Linux 子系统3.2 安装位置3.3 更换源4. 安装图形化界面4.1 安装VcXsrv4.2 安装桌面环境&#xff08;1&#xff09;方法1&#xff1a;VcXsrv Gnome&#xff08;2&#xff09;方法2&#xff1a;VcXsrv Xfce4…

Python到底牛在哪?现在就业薪资高吗?

Python是什么呢&#xff1f;Python是一种全栈的开发语言&#xff0c;你如果能学好Python&#xff0c;前端&#xff0c;后端&#xff0c;测试&#xff0c;大数据分析&#xff0c;爬虫等这些工作你都能胜任。当下Python有多火我不再赘述,Python有哪些作用呢&#xff1f;据我多年P…

GoogleTest中gMock的使用

GoogleTest中的gMock是一个库&#xff0c;用于创建mock类并使用它们。 当你编写原型或测试(prototype or test)时&#xff0c;完全依赖真实对象通常是不可行或不明智的(not feasible or wise)。模拟对象(mock object)实现了与真实对象相同的接口,但是需要你在运行时指定它…

SpringCloud学习笔记 - Sentinel流控规则配置的持久化 - Sentinel

1. 为什么要将流控规则持久化 默认的的流控规则是配置在sentinel中的&#xff0c;又因为sentinel是懒加载的&#xff0c;只有当我们访问了一个请求的时候&#xff0c;sentinel才能监控到我们的簇点链路&#xff0c;我们才能对该链路进行流控配置&#xff0c;一旦我们重启应用s…

GNN专栏总览

文章目录图卷积神经网络1. 理论篇2. 模型篇3. 有关gnn的论文检索图卷积神经网络 1. 理论篇 原理&#xff1a;http://xtf615.com/2019/02/24/gcn/论文&#xff1a; 综述类&#xff1a; HOW POWERFUL ARE GRAPH NEURAL NETWORKS?Bridging the Gap between Spatial and Spectra…

PHP实现个人免签约微信支付接口原理+源码

什么是个人免签支付 个人免签支付就是给个人用的支付接口&#xff0c;一般的支付接口都需要营业执照才能申请&#xff0c;个人很难申请的到&#xff0c;或者是没有资质去申请&#xff0c;要和支付商进行签约的。免签&#xff0c;顾名思义就是不需要签约。那么个人免签支付就有…

企业数字化运营平台软件开发框架项目

【版权声明】本资料来源网络&#xff0c;知识分享&#xff0c;仅供个人学习&#xff0c;请勿商用。【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间删除&#xff01;完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 目录 1 项目总体概述 1.1 项目…

Unity Avatar Camera Controller 第一、第三人称相机控制

文章目录简介Variables实现Target PositionTarget RotationOthers简介 本文介绍如何实现用于Avatar角色的相机控制脚本&#xff0c;支持第一人称、第三人称以及两种模式之间的切换&#xff0c;工具已上传至SKFramework框架的Package Manager中&#xff1a; Variables Avatar&…

51单片机入门 - 简短的位运算实现扫描矩阵键盘

介绍 例程使用 SDCC 编译、 stcgal 烧录&#xff0c;如果你想要配置一样的环境&#xff0c;可以参考本专栏的第一篇文章“51单片机开发环境搭建 - VS Code 从编写到烧录”&#xff0c;我的设备是 Windows 10&#xff0c;使用普中51单片机开发板&#xff08;STC89C52RC&#xf…

Qt编写微信支付宝支付

文章目录一 微信支付配置参数二 支付宝支付配置参数三 功能四 Demo效果图五 体验地址一 微信支付配置参数 微信支付API&#xff0c;需要三个基本必填参数。 微信公众号或者小程序等的appid&#xff1b;微信支付商户号mchId&#xff1b;微信支付商户密钥mchKey&#xff1b; 具…

文件基础IO

目录 前言 用库进行文件操作 文件描述符 理解Linux一切皆文件 缓冲区 认识缓冲区 缓冲区缓冲策略 磁盘结构 磁盘分区 软链接和硬链接 硬链接本质 软连接本质 动态库和静态库进阶 写一个静态库 动态库的产生和使用 动静态库的加载 总结&#xff1a; 前言 在我们了…

SE | 哇哦!让人不断感叹真香的数据格式!~

1写在前面 最近在用的包经常涉及到SummarizedExperiment格式的文件&#xff0c;不知道大家有没有遇到过。&#x1f912; 一开始觉得这种格式真麻烦&#xff0c;后面搞懂了之后发现真是香啊&#xff0c;爱不释手&#xff01;~&#x1f61c; 2什么是SummarizedExperiment 这种cla…

lighthouse的介绍和基本使用方法

Lighthouse简介 Lighthouse是一个开源的自动化性能测试工具&#xff0c;我们可以使用该功能检测我们的页面存在那些性能方面的问题&#xff0c;并会生成一个详细的性能报告来帮助我们来优化页面 使用方式 LH一共有四种使用方式 Chrome开发者工具Chrome扩展Node 命令行Node …

数据结构与算法(一)-软件设计(十七)

设计模式&#xff08;十五&#xff09;-面向对象概念https://blog.csdn.net/ke1ying/article/details/129171047 数组 存储地址的计算&#xff1a; 一维数组a[n]&#xff0c;当a[2]的存储地址为&#xff1a;a2*len&#xff0c;如果每一个数组元素只占用一个字节&#xff0c;那…

Spring Batch 高级篇-分区步骤

目录 引言 概念 分区器 分区处理器 案例 转视频版 引言 接着上篇&#xff1a;Spring Batch 高级篇-并行步骤了解Spring Batch并行步骤后&#xff0c;接下来一起学习一下Spring Batch 高级功能-分区步骤 概念 分区&#xff1a;有划分&#xff0c;区分意思&#xff0c;在…

中国ETC行业市场规模及未来发展趋势

中国ETC行业市场规模及未来发展趋势编辑根据市场调研在线网发布的2023-2029年中国ETC行业发展策略分析及战略咨询研究报告分析&#xff1a;随着政府坚持实施绿色出行政策&#xff0c;ETC行业也受到了极大的支持。根据中国智能交通协会统计&#xff0c;2017年中国ETC行业市场规模…