深入理解CyclicBarrier

news2024/9/22 9:30:19

CyclicBarrier翻译过来是回环栅栏,它可以实现让一组线程等待至某个状态之后再全部同步执行,这个状态叫屏障点。当所有等待线程都被释放后,CyclicBarrier可以重置再次使用。

CyclicBarrier的功能是由ReentrantLock和Condition共同实现的,因此在其内部拥有ReentrantLock类型的lock属性和Condition类型的trip属性。此外,还有用于保存该屏障拦截的线程数parties属性和当前剩余等待的线程数count属性。这些属性的作用在后面我们详细介绍源码时再详细介绍。

CyclicBarrier的构造函数如下:

//parties表示当前屏障拦截的线程数
public CyclicBarrier(int parties) {
    this(parties, null);
}
//barriesAction表示当所有线程都到达屏障时首先执行的行为
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

常用方法如下:

  • public int await() throws InterruptedException, BrokenBarrierException:当前线程阻塞,直到parties个线程全部调用await()方法时再唤醒。BrokenBarrierException异常表示栅栏已被破坏,可能是由于其中的一个线程await()时被中断或超时;
  • public void reset():重置屏障计数器的值,并将条件队列的所有线程唤醒。

由于CyclicBarrier的计数器可以重置,屏障可以重复使用,因此当paries的整数倍数量的线程调用await()方法时程序都是可以正常结束的,否则由于还有线程在阻塞,程序会一直阻塞不会结束。例如下面的程序中定义了屏障的拦截线程数paties=3,随后分别在7个线程中调用CyclicBarrier的await()方法。

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 7; i++) {
            new Thread(() -> {
				try {
					System.out.println(Thread.currentThread().getName()
							+ "开始等待其他线程");
					cyclicBarrier.await();
					System.out.println(Thread.currentThread().getName() + "开始执行");
					Thread.sleep(5000);
					System.out.println(Thread.currentThread().getName() + "执行完毕");

				} catch (Exception e) {
					e.printStackTrace();
				}
			}).start();
        }
    }
}

控制人打印结果如下图,可以发现程序没有结束,因此线程Thread-6还在阻塞。

CyclicBarrier原理

由于CyclicBarrier是借助ReentrantLock和Condition实现的,因此我们在深入解析CyclicBarrier源码之前先来简单介绍下ReentrantLock和Condition是怎么工作的。

ReentrantLock和Condition

我们都知道AQS是基于MESA管程模型实现的,ReentrantLock是AQS的独占锁实现,而Condition可以理解为是MESA模型中的条件变量,那么当执行下面的程序时锁(ReentrantLock)是怎么变化的呢?需要注意的是,下面的程序中只有一个ReentrantLock对象,因此只有一把锁。

public class ReentrantLockDemo6 {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition cigCon = lock.newCondition();
    private static Condition takeCon = lock.newCondition();

    private static boolean hashcig = false;
    private static boolean hastakeout = false;

    //送烟
    public void cigratee(){
        lock.lock();
        try {
            while(!hashcig){
                try {
                    log.debug("没有烟,歇一会");
                    cigCon.await();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            log.debug("有烟了,干活");
        }finally {
            lock.unlock();
        }
    }

    //送外卖
    public void takeout(){
        lock.lock();
        try {
            while(!hastakeout){
                try {
                    log.debug("没有饭,歇一会");
                    takeCon.await();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            log.debug("有饭了,干活");
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo6 test = new ReentrantLockDemo6();
        new Thread(() ->{
            test.cigratee();
        },"t1").start();

        new Thread(() -> {
            test.takeout();
        },"t2").start();

        new Thread(() ->{
            lock.lock();
            try {
                hashcig = true;
                log.debug("唤醒送烟的等待线程");
                cigCon.signal();
            }finally {
                lock.unlock();
            }


        },"t3").start();

        new Thread(() ->{
            lock.lock();
            try {
                hastakeout = true;
                log.debug("唤醒送饭的等待线程");
                takeCon.signal();
            }finally {
                lock.unlock();
            }


        },"t4").start();
    }

}

控制台打印结果如下:

  1. 线程t1执行cigratee()方法,cigratee()内部执行lock()方法加锁(即将当前lock对象的state设为1),随后cigCon调用await()方法释放锁(即将当前lock对象的state设为0),并将线程t1阻塞;
  2. 线程t2执行takeout()方法,takeout()内部执行lock()方法加锁(即将当前lock对象的state设为1),随后takeCon调用await()方法释放锁(即将当前lock对象的state设为0),并将线程t2阻塞;
  3. 线程t3调用cigCon.signal()方法将阻塞在cigCon这个“条件变量”的条件队列线程t1转移到lock的同步等待队列中,当线程t1获取锁后继续执行后面的逻辑,执行完后再调用unlock()方法释放锁;
  4. 线程t4调用takeCon.signal()方法将阻塞在takeCon这个“条件变量”的条件队列线程t2转移到lock的同步等待队列中,当线程t2获取锁后继续执行后面的逻辑,执行完后再调用unlock()方法释放锁。

关于以上,需要做更多的说明:

  • ReentrantLock加锁的方式是将state设为1,并且保存持有锁的线程引用;解锁的方式是将state设为0,并释放线程引用;
  • Condition的await()方法会释放锁,且阻塞当前线程,这一点可以从程序输出结果得到;
  • Condition的signal()方法在唤醒阻塞在当前条件变量上的线程时,需要首先获取锁,否则会抛出IllegalMonitorStateException异常;
  • 按照MESA模型,调用Condition的signal()方法后,会将当前条件变量的条件队列的线程转移到锁的同步等待队列中,当获取锁时才会继续执行后续逻辑。

我们可以通过跟踪CyclicBarrier的源码来达到学习CyclicBarrier以及ReentrantLock与Condition协作的目的。

CyclicBarrier源码

这里是从CyclicBarrier的await()方法一步一步地介绍的,较复杂,如果不想看可以直接跳到这一部分后面总结的await()方法执行的流程图部分。

我们使用下面的程序来切入CyclicBarrier源码的介绍。在下面的程序中,使用三个线程分别计算三位同学的平均分,三个线程都计算完成后再计算三位同学的平均分。

public class CyclicBarrierTest2 {

    //保存每个学生的平均成绩
    private ConcurrentHashMap<String, Integer> map=new ConcurrentHashMap<String,Integer>();

    private ExecutorService threadPool= Executors.newFixedThreadPool(3);

    private CyclicBarrier cb=new CyclicBarrier(3,()->{
        int result=0;
        Set<String> set = map.keySet();
        for(String s:set){
            result+=map.get(s);
        }
        System.out.println("三人平均成绩为:"+(result/3)+"分");
    });


    public void count(){
        for(int i=0;i<3;i++){
            threadPool.execute(new Runnable(){

                @Override
                public void run() {
                    //获取学生平均成绩
                    int score=(int)(Math.random()*40+60);
                    map.put(Thread.currentThread().getName(), score);
                    System.out.println(Thread.currentThread().getName()
                            +"同学的平均成绩为:"+score);
                    try {
                        //执行完运行await(),等待所有学生平均成绩都计算完毕
                        cb.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }

            });
        }
    }


    public static void main(String[] args) {
        CyclicBarrierTest2 cb=new CyclicBarrierTest2();
        cb.count();
    }
}

当线程t1首先计算完成执行cb.await()方法,await()方法实现如下:

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

dowait()方法实现如下:

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        //1.使用CyclicBarrier的lock对象加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            //2.当前线程到达屏障,因此count数量-1,判断计算后的值是否为0
            int index = --count;
            if (index == 0) { 
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            //3.当前线程阻塞,直到被唤醒、中断或超时
            for (;;) {
                try {
                    if (!timed)
                        //4.调用CyclicBarrier的Condition的await()方法
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
  1. 前面提到过CyclicBarrier中有一个ReentrantLock类型的属性lock,此处使用lock对象加锁,目的其实是为了后面Condition的await()方法的调用。需要注意的是,按照MESA模型,线程进入条件队列必须是在其获得锁之后,因此此处必须先加锁;
  2. 此时count = parties = 3,线程t1已到达屏障处,因此count需要减1,此时count=2,parties仍为3,parties的值是不会变的,原因后面会介绍;
  3. 由于此时count!=0,因此我们直接跳到4处,调用CyclicBarrier的Condition类型的属性trip的await()方法。

这里需要特别说明的是,Condition是一个接口,其实现类是AQS中的一个内部类ConditionObject,在其内部维护了条件等待队列的首节点Node对象firstWaiter和尾结点lastWaiter,另外还包括一些维护条件队列出队入队操作的方法。此处调用的trip的await()方法当然是Condition接口的await()方法,实现如下:

public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
    	//1
        Node node = addConditionWaiter();
    	//2
        int savedState = fullyRelease(node);
        int interruptMode = 0;
    	//3
        while (!isOnSyncQueue(node)) {
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
    	//4
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

首先进入addConditionWaiter()方法,实现如下:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
  1. 由于此时条件等待队列还是空队列,因此尾节点lastWaiter为null;
  2. 将线程t1封装到Node对象中,并且将当前Node节点的状态设为CONDITION(值为-2)表示该节点是条件等待队列的节点;
  3. 将firstWaiter首节点和尾节点lastWaiter都指向线程t1的Node节点,表示t1节点入队。

然后回到await()方法,进入2处的fullRelease()方法,其实现逻辑如下:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

需要注意的是,此时还在CyclicBarrier的lock锁内,此处调用release()方法释放锁(主要就是将state设为0,ReentrantLock的表示独占锁拥有者的exclusiveOwnerThread属性置为null),关于release()方法我们在介绍ReentrantLock的文章中介绍过,此处不再赘述。

再返回await()方法的代码3处,isOnSyncQueue()方法的实现如下:

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) 
        return true;
    return findNodeFromTail(node);
}

由于线程t1节点是在条件等待队列中,其状态是CONDITION,因此此处直接返回false。

再返回await()方法,可以看到isOnSyncQueue()方法返回false后就执行了LockSupport的park()方法将当前线程t1阻塞。

此时线程t1内部执行CyclicBarrier的await()方法的逻辑就暂时完成了,总结如下:

  1. 使用CyclicBarrier的lock对象加锁;
  2. 将CyclicBarrier的计数器count的值减1,此时count=2;
  3. 判断count不为0,调用Condition类型的trip属性的await()方法;
  4. 将当前线程t1封装到Node节点对象中,状态为CONDITION,表示该节点在条件等待队列中,将线程t1节点入队条件等待队列,此时firstWaiter和lastWaiter指向的都是线程t1节点;
  5. 释放lock锁;
  6. 将线程t1阻塞。

随后线程t2也执行到了await()方法,此处我们不再一步步地跟踪了,其执行逻辑如下:

  1. 使用CyclicBarrier的lock对象加锁;
  2. 将CyclicBarrier的计数器count的值减1,此时count=1;
  3. 判断count不为0,调用Condition类型的trip属性的await()方法;
  4. 将当前线程t2封装到Node节点对象中,状态为CONDITION,表示该节点在条件等待队列中,将线程t2节点入队条件等待队列,此时firstWaiter指向线程t1节点,lastWaiter指向线程t2节点;
  5. 释放lock锁;
  6. 将线程t2阻塞。

最后线程t3也执行到了await()方法,前两个步骤与线程t1和t2一样:

  1. 使用CyclicBarrier的lock对象加锁;
  2. 将CyclicBarrier的计数器count的值减1,此时count=0;

在第三个步骤dowait()方法,判断count的值为0,此处与线程t3与前面的线程逻辑不再一样了,相关部分代码如下:

if (index == 0) {  // tripped
    boolean ranAction = false;
    try {
        final Runnable command = barrierCommand;
        if (command != null)
            command.run();
        ranAction = true;
        nextGeneration();
        return 0;
    } finally {
        if (!ranAction)
            breakBarrier();
    }
}

此处由于计数器count=0,已经到了屏障处,因此首先执行我们在构造函数中传入的barrierCommand任务。随后调用nextGeneration()方法,实现如下:

private void nextGeneration() {
    trip.signalAll();
    count = parties;
    generation = new Generation();
}

可以看到又调用了Condition的signalAll()方法,实现如下:

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

首先判断当前线程t2是否是获取独占锁的线程,是的。随后判断条件队列的首节点即线程t1节点不为空,调用doSignalAll()方法,实现如下:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
  1. 将条件队列的首节点和尾节点置为null;
  2. 将首节点(线程t1节点)出队,执行transferForsignal()方法;
  3. 循环条件队列,不断地将节点出队,调用transferForsignal()方法。

在我们这个例子中循环了两次,分别为线程t1和线程t2的节点执行transferForsignal()方法,transferForsignal()方法实现如下:

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
  1. 使用CAS将节点的waitStatus置为0,在此之前条件队列节点的状态为CONDITION(-2);
  2. 调用enq()方法将节点入队到AQS的同步等待队列中,此方法在介绍ReentrantLock时也介绍过,不再赘述;
  3. 随后使用CAS将节点在同步等待队列中的前一个节点的waitStatus置为-1,表示当前节点可以被唤醒。

线程t2的逻辑到此处就执行完成了,一层层的方法返回,最终CyclicBarrier的doWait()方法的finally块中执行lock.unpark()方法。unpark()方法唤醒的是当前lock对象的同步等待队列的头节点,在当前lock锁竞争激烈的场景下,头节点有可能不是线程t1或者线程t2。此处需要注意的是,当线程t1或线程t2获得CPU继续执行时,应该在它们被阻塞的地方继续执行,即在AQS的ConditionObject的await()方法的代码4处(翻到前面的代码块)继续执行。继续执行的acquireQueued()方法是循环调用CAS获取lock锁的逻辑,因此线程t1和t2在此处会再次获取到锁,执行我们自己的代码中CyclicBarrier的await()方法之后的逻辑。

CyclicBarrier的await()逻辑流程图

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

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

相关文章

java: 无法访问org.springframework.boot.SpringApplication 错误的类文件

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; java: 无法访问org.springframework.boot.SpringApplication 错误的类文件: /D:/Software/env-java/apache-maven-3.6.1/repository/org/springframework/boot/spring-boot/3.1.2/spring-boot-3.1.2.ja…

Docker基础操作入门

1 初识 Docker 1.1 简介 我们写的代码会接触到好几个环境&#xff1a;开发环境、测试环境以及生产环境 Docker概念 Docker 是一个开源的应用容器引擎&#xff0c;诞生于 2013 年初&#xff0c;基于 Go 语言实现&#xff0c; dotCloud 公司出品&#xff08;后改名为Docker In…

钝感力:项目经理的隐形盾牌

钝感力的定义 钝感力的起源和意义 钝感力&#xff0c;或者说对外部刺激的不敏感性&#xff0c;起源于我们的生存本能。在古代&#xff0c;人们面对各种危险和挑战&#xff0c;需要有一定的抗压能力&#xff0c;才能在恶劣环境中生存下来。这种能力使得我们的祖先能够在面对猛兽…

R package org.Hs.eg.db to convert gene id

文章目录 install使用org.Hs.egENSEMBL将Ensembl id convert to gene idorg.Hs.egGENENAME 将Ensembl id convert to gene nameorg.Hs.egSYMBOL 将 gene symbol convert to gene id我现在有一些ensembl id 如何转为 gene name注意你会遇到一些record不全的情况&#xff0c;gtf文…

Certify The Web (IIS)

一、简介 Certify The Web 适用于 Windows的SSL 证书管理器用户界面&#xff0c;与所有 ACME v2 CA 兼容&#xff0c;为您的 IIS/Windows 服务器轻松地安装和自动更新来自 Letencrypt.org 和其他 ACME 证书授权机构的免费 SSL/TLS 证书&#xff0c;设置 https 从未如此简单。 …

T599聚合物电容器:在汽车应用中提供更长的使用寿命的解决方案

自从电子技术被引入汽车工业以来&#xff0c;汽车的技术含量一直在提升。诸多技术被应用在汽车上&#xff0c;使汽车的形象更接近于轮子上的超级计算机。更多传感器、更强大的计算能力和电力被装载到汽车上&#xff0c;汽车应用中的电子产品数量正在迅速增长。随着电动汽车和自…

jeecg如何创建报表并配置到菜单中

当使用jeecg创建单表之后,需要进行报表显示,并把报表配置到菜单中,该如何操作呢?下面进行详细讲解。这里以课程表这张表为例进行讲解。 一.表单创建完成,并配置好菜单栏。具体步骤略,如下图: 二.创建积木报表 1.左侧边栏展开低代码开发菜单,进入报表设计器栏目 2.进…

数据库中字段不给值或给““,默认存什么

准备 在mysql数据库中有一张stu表,下面是表的结构和数据和Student类 开始测试一(不给值) 情况一:添加一条数据,但是跳过math列 可以看到,表的int类型的默认为null. 查id2的这条数据,用student类去接 结果math是0,因为实体类的int元素不会是null,会默认给个0 用hashMap<st…

Vue教程(五):样式绑定——class和style

1、样式代码准备 样式提前准备 <style>.basic{width: 400px;height: 100px;border: 1px solid black;}.happy{border: 4px solid red;background-color: rgba(255, 255, 0, 0.644);background: linear-gradient(30deg, yellow, pink, orange, yellow);}.sad{border: 4px …

【Java 高阶】一文精通 Spring MVC - 基础概念(一)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

微信小程序使用npm引入三方包详解

目录 1 前言2 微信小程序npm环境搭建2.1 创建package.json文件2.2 修改 project.config.json2.3 修改project.private.config.json配置2.4 构建 npm 包2.5 typescript 支持2.6 安装组件2.7 引入使用组件 1 前言 从小程序基础库版本 2.2.1 或以上、及开发者工具 1.02.1808300 或…

课程项目设计--spring security--用户管理功能--宿舍管理系统--springboot后端

写在前面&#xff1a; 还要实习&#xff0c;每次时间好少呀&#xff0c;进度会比较慢一点 本文主要实现是用户管理相关功能。 前文项目建立 文章目录 验证码功能验证码配置验证码生成工具类添加依赖功能测试编写controller接口启动项目 security配置拦截器配置验证码拦截器 …

一、pikachu之SQL注入

文章目录 一、SQL注入步骤二、数字型注入(post注入)三、字符型注入(get)四、搜索型注入五、XX型注入六、“insert/updata”注入 一、SQL注入步骤 寻找传参页面&#xff1b;判断是否存在注入点&#xff1b; 字符型注入&#xff1f;数字型注入&#xff1f; 判断字段的数量&#…

二、4.makefile、断言和位图内存池

在 Linux 中&#xff0c;文件分为属性和数据两部分&#xff0c;每个文件有三种时间&#xff0c;分别用于记录与文件属性和文件数据相关的时间&#xff0c;这三个时间分别是 atime、 mtime 、 ctime 。 atime&#xff0c;即 access time&#xff0c;和示访问文件数据部分时间&a…

SprintBoot Bean管理

SpringBoot中获取Bean对象 下面这段代码在测试类中进行&#xff0c;下面通过三种方式获取bean对象。 import org.springframework.context.ApplicationContext;// 注意一定要引入上面的依赖SpringBootTest class TliasWebManagementApplicationTests {Testvoid getBeanTest(){…

AI夏令营第三期 - 基于论文摘要的文本分类与关键词抽取挑战赛笔记

赛题&#xff1a;基于论文摘要的文本分类与关键词抽取 背景&#xff1a;高效的从海量医学文献中提取疾病诊断和治疗关键信息 任务&#xff1a;通过论文摘要判断论文是否为医学文献 样例 数据集&#xff1a;csv文件&#xff0c;字段&#xff1a;标题、作者、摘要、关键词 评价指…

uniapp 引入海康H5player实现视频监控的播放

uniapp直接调用海康H5player方法&#xff0c;只能在web浏览器页面正常播放&#xff0c;实机运行会因为找不到文件的相对路径而报错无法播放。因此需要通过web-view或iframe引入html的方式来实现实时视频监控的播放。具体步骤如下&#xff1a; 1、首先将海康h5player的相关文件…

服装定制小程序的秘诀

随着互联网的快速发展&#xff0c;越来越多的企业开始关注互联网商业模式的创新。其中&#xff0c;定制化服务成为了各行各业的关注焦点之一。尤其是在服装行业&#xff0c;定制化服装已经成为许多消费者的追求。而面对这一市场需求&#xff0c;如何创造成功的互联网新商业模式…

https非对称加密算法

非对称加密算法原理 在客户端公开公钥&#xff0c;服务端保存私钥 1.客户端第一次请求先请求443端口&#xff0c;从443端口下载公钥。 2.客户端将数据进行公钥算法进行加密&#xff0c;将秘文发送到服务端 服务端收到秘文后&#xff0c;通过私钥算法进行解密得到明文数据。…

【STM32】开发方式:寄存器、CMSIS、SPL、HAL、LL、RTOS

阅读本专栏其他文章&#xff0c;有助于理解本文。 文章目录 一、开发库选择1.1 概述1.2 CMSIS库1.3 SPL库1.4 HAL 库1.5 LL库1.6 寄存器开发 二、代码对比2.1 使用寄存器2.2 使用CMSIS库2.3 使用SPL库2.4 使用HAL库2.5 使用LL库2.6 使用RTOS 三、软件配置 一、开发库选择 1.1 …