JUC并发编程学习笔记(三)集合线程安全及多线程锁

news2024/9/23 3:29:52

4 集合的线程安全

4.1 集合操作 Demo

创建集合使用string的泛型

for (int i = 0; i <30; i++) {
    new Thread(()->{
        //向集合添加内容
       list.add(UUID.randomUUID().toString().substring(0,8));
        //从集合获取内容
        System.out.println(list);
    },String.valueOf(i)).start();
}

查看源码,主要牵扯这个添加的方法是因为该方法在列表中是不安全的,没有synchronized声明

boolean add(E e);

报错

在这里插入图片描述

java.util.ConcurrentModificationException并发修改问题

4.2 vector

Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList,实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能**。 Vector 实现了 RandmoAccess 接口,即**提供了随机访问功能。RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。

和 ArrayList 不同,Vector 中的操作是线程安全的。

通过list下的实现类Vector
因为在Vector下的add普遍都是线程安全
查看源代码

public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
    }

在改变代码时,只需要将其修改为List<String> list = new Vector<>();
但此方法用的比较少,因为在jdk 1.0的版本适用

4.3 Collections

Collections类中的很多方法都是static静态
其中有一个方法是返回指定列表支持的同步(线程安全的)列表为synchronizedList(List <T> list)
具体代码为

List<String> list = Collections.synchronizedList(new ArrayList<>());

此方法也比较古老,很少使用

4.4 CopyOnWriteArrayList

首先我们对 CopyOnWriteArrayList 进行学习,其特点如下:

它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和ArrayList 不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
  2. 它是线程安全的。
  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
  4. 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

1. 独占锁效率低:采用读写分离思想解决

2. 写线程获取到锁,其他写线程阻塞

3. 复制思想:

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来 得及写会内存,其他的线程就会读到了脏数据

这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。

将其代码修改为

List<String> list = new CopyOnWriteArrayList<>();

涉及的底层原理为写时复制技术

  • 读的时候并发(多个线程操作)

  • 写的时候独立,先复制相同的空间到某个区域,将其写到新区域,旧新合并,并且读新区域(每次加新内容都写到新区域,覆盖合并之前旧区域,读取新区域添加的内容)

  •  public boolean add(E e) {
            synchronized (lock) {
                Object[] es = getArray();
                int len = es.length;
                es = Arrays.copyOf(es, len + 1);
                es[len] = e;
                setArray(es);
                return true;
            }
        }
    

原因分析(重点):动态数组与线程安全

下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList 的原理进行说明。

  1. “动态数组”机制
    1. 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
    2. 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的 操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话, 效率比较高。
  2. “线程安全”机制
    1. 通过 volatile 和互斥锁来实现的。
    2. 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
    3. 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。

4.5 CopyOnWriteArraySet

该类是HashSet的实现类
同样使用HashSet类,也会出现线程不安全
java Set<String> set = new HashSet<>();
需要将上面的代码改为
Set<String> set = new CopyOnWriteArraySet<>();

for (int i = 0; i <30; i++) {
    new Thread(()->{
        //向集合添加内容
        set.add(UUID.randomUUID().toString().substring(0,8));
        //从集合获取内容
        System.out.println(set);
    },String.valueOf(i)).start();
}

4.6 ConcurrentHashMap

ConcurrentHashMap类是HashMap的实现类
先讲述其线程不安全实列

HashMap不安全线程也同理Map<String,String> map = new HashMap<>();
具体实现代码是

for (int i = 0; i <30; i++) {
    String key = String.valueOf(i);
    new Thread(()->{
        //向集合添加内容
        map.put(key,UUID.randomUUID().toString().substring(0,8));
        //从集合获取内容
        System.out.println(map);
    },String.valueOf(i)).start();
}

将其代码修改为
Map<String,String> map = new ConcurrentHashMap<>();
通过这行代码可以编程线程安全

4.7 volatile关键字的作用

1、保证可见性;
2、防止指令重排;
3、但是不保证原子性;

4.7.1 可见性是什么?

在JMM(java memory model)java内存模型中,其他线程从主内存空间把值拷贝到自己的工作空间,线程修改之后的值会返回给主内存,主内存会通知其他线程,此为可见性

4.7.2 指令重排

CPU为了执行效率会并发执行操作指令,volatile可以使指令一个一个的执行

4.7.3 如何解决原子性问题

1、通过synchronized关键字。
2、通过使用AtomicXX,不加锁,采用CAS(compareAndSet)解决。其本质是使用UnSafe本地方法(CPU原语)。
3、使用LongAdder:最快(在线程多的情况下,使用分段锁)

4.8 写时复制(Copy-on-write)

简称COW)是一种计算机程序设计领域的优化策略。

核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的(transparently)。

此作法的主要优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。

JDK的CopyOnWriteArrayList/CopyOnWriteArraySet容器就采用了COW思想。

4.8.1 vector的缺陷

容器是线程安全的,并不意味着就可以在多线程环境下放心大胆地随便。

在一个线程中使用Iterator迭代器遍历vector,同时另一个线程对vector作修改时,会抛出java.util.ConcurrentModificationException异常。

4.8.2 实现原理

Copy-on-write是解决并发的的一种思路,指的是实行读写分离,如果执行的是写操作,则复制一个新集合,在新集合内添加或者删除元素。待一切修改完成之后,再将原集合的引用指向新的集合。好处就是,可以高并发地对COW进行读和遍历操作,而不需要加锁,因为当前集合不会添加任何元素。

CopyOnWriteArrayList的核心理念就是读写分离,写操作在一个复制的数组上进行,读操作还是在原始数组上进行,读写分离,互不影响。写操作需要加锁,防止并发写入时导致数据丢失。写操作结束之后需要让数组指针指向新的复制数组。

4.8.3 CopyOnWriteArrayList优缺点总结

优点

  • 对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
  • CopyOnWriteArrayList并发安全且性能比Vector好。Vector是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector。

缺点

  • 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了,但是线程A迭代出来的是旧数据。
  • 内存占用问题。如果CopyOnWriteArrayList经常要增删改里面的数据,并且对象比较大,频繁地写会消耗内存,从而引发Java的GC问题,这个时候,我们应该考虑其他的容器,例如ConcurrentHashMap。

4.9 小结(重点)

1.线程安全与线程不安全集合

集合类型中存在线程安全与线程不安全的两种,常见例如:

ArrayList ----- Vector

HashMap -----HashTable

但是以上都是通过 synchronized 关键字实现,效率较低

2.Collections 构建的线程安全集合

3.java.util.concurrent 并发包下

CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全

5 多线程锁

某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象

synchronized锁的是方法,则是对象锁
同个对象锁的机制要等待,不同对象锁的机制调用同一个不用等待
加了static则为class锁而不是对象锁

5.1 锁的八个问题演示


class Phone {

    public synchronized void sendSMS() throws Exception {
        //停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }
}

通过调用锁的对象机制

public class Lock_8 {
    public static void main(String[] args) throws Exception {

        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
               // phone.sendEmail();
               // phone.getHello();
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

具体八种情况为

1 标准访问,先打印短信还是邮件
------sendSMS
------sendEmail
2 停4秒在短信方法内,先打印短信还是邮件
------sendSMS
------sendEmail
3 新增普通的hello方法,是先打短信还是hello
------getHello
------sendSMS
4 现在有两部手机,先打印短信还是邮件
------sendEmail
------sendSMS
5 两个静态同步方法,1部手机,先打印短信还是邮件
------sendSMS
------sendEmail
6 两个静态同步方法,2部手机,先打印短信还是邮件
------sendSMS
------sendEmail
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
------sendEmail
------sendSMS
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
------sendEmail
------sendSMS

5.2 公平锁和非公平锁

  • 公平锁:效率相对低
  • 非公平锁:效率高,但是线程容易饿死

通过查看源码
带有参数的ReentrantLock(true)公平锁
ReentrantLock(false)非公平锁
主要是调用NonfairSync()FairSync()

public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

具体其非公平锁与公平锁的源码
查看公平锁的源码

static final class FairSync extends Sync {
   private static final long serialVersionUID = -3000897897090466540L;

  /**
  * Acquires only if reentrant or queue is empty.
   */
  final boolean initialTryLock() {
   Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) {
   if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
     setExclusiveOwnerThread(current);
      return true;
    }
    } else if (getExclusiveOwnerThread() == current) {
      if (++c < 0) // overflow
          throw new Error("Maximum lock count exceeded");
         setState(c);
         return true;
       }
    return false;
}

private final ReentrantLock lock = new ReentrantLock();默认是初始化一个非公平锁,如果要实现公平锁,private final ReentrantLock lock = new ReentrantLock(true);

5.3 可重入锁

synchronized和lock都是可重入锁(也叫递归锁)

  • sychronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁
  • 可重入锁也叫递归锁

而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构

Object o = new Object();
new Thread(()->{
    synchronized(o) {
        System.out.println(Thread.currentThread().getName()+" 外层");

        synchronized (o) {
            System.out.println(Thread.currentThread().getName()+" 中层");

            synchronized (o) {
                System.out.println(Thread.currentThread().getName()+" 内层");
            }
        }
    }

},"t1").start();

synchronized (o)代表锁住当前{ }内的代码块

以上都是synchronized锁机制
下面讲解lock锁机制

public class SyncLockDemo {

    public synchronized void add() {
        add();
    }

    public static void main(String[] args) {
        //Lock演示可重入锁
        Lock lock = new ReentrantLock();
        //创建线程
        new Thread(()->{
            try {
                //上锁
                lock.lock();
                System.out.println(Thread.currentThread().getName()+" 外层");

                try {
                    //上锁
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+" 内层");
                }finally {
                    //释放锁
                    lock.unlock();
                }
            }finally {
                //释放做
                lock.unlock();
            }
        },"t1").start();

        //创建新线程
        new Thread(()->{
            lock.lock();
            System.out.println("aaaa");
            lock.unlock();
        },"aa").start();
        }
 }

在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁
要把握上锁与解锁的概念,都要写上

5.4 死锁(面试重点)

两个或以上的进程因为争夺资源而造成互相等待资源的现象称为死锁

在这里插入图片描述

产生死锁的原因:

  1. 系统资源不足
  2. 系统资源分配不当
  3. 进程运行顺序不当

具体死锁的操作代码实列
可理解背下来,大厂面试可考,死锁的简单案例

public class DeadLock {

    //创建两个对象
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (a) {
                System.out.println(Thread.currentThread().getName()+" 持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁b");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (b) {
                System.out.println(Thread.currentThread().getName()+" 持有锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁a");
                }
            }
        },"B").start();
    }
}

验证是否是死锁(要求掌握)

  1. jps 类似于linux中的ps -ef查看进程号
  2. jstack 自带的堆栈跟踪工具

通过用idea自带的命令行输入 jps -l
查看其编译代码的进程号后jstack 进程号

5.5 总结

一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法。

锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized 方法

加个普通方法后发现和同步锁无关

换成两个对象后,不是同一把锁了,情况立刻变化。

synchronized实现同步的基础: Java中的每一个对象都可以作为锁。具体表现为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Clas对象
  • 对于同步方法块,锁是Synchonized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

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

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

相关文章

Odoo 16 企业版手册 - 库存管理之产品变体与属性

产品变体和属性 向客户提供多种产品变体是提高销售的有效策略。Odoo允许您在库存模块中配置产品的不同变体&#xff0c;就像我们在销售和购买模块中所做的那样。首先&#xff0c;确保从库存模块的「设置」菜单中激活「变体」功能&#xff0c;如下所示。 「产品变体」选项可以从…

【MySQL基础教程】事务详细介绍

前言 本文为 【MySQL基础教程】事务 相关知识&#xff0c;下边将对事务简介&#xff0c;事务操作&#xff08;包括&#xff1a;未控制事务&#xff0c;控制事务&#xff09;&#xff0c;事务四大特性&#xff0c;并发事务问题&#xff0c;事务隔离级别等进行详尽介绍~ &#x…

Linux安装mongodb社区版

1、下载资源 Download MongoDB Community Server | MongoDB 2、将下载的安装包上传到服务器上&#xff0c;并解压缩 #解压 tar -zxvf mongodb-linux-x86_64-rhel70-6.0.3.tgz #移动路径重命名mv mongodb-linux-x86_64-rhel70-6.0.3 /usr/local/mongodb 3、进入 /usr/local/…

适合中学生看的英文电影

怎样利用好丰富的资源来学习英语口语呢&#xff1f;其实其实看什么样的剧、如何看剧都是很讲究的。一起来解锁吧。 一、选剧要学会拆解自己学习目标&#xff0c;选定合适的类型&#xff0c;各取所需。 并不是所有类型的国外影视剧都适合作为学习的素材&#xff0c;主要依据自身…

Python Pyecharts柱状横向动态图-全球GDP比较

程序示例精选 Python Pyecharts柱状横向动态图 如需安装运行环境或远程调试&#xff0c;见文章底部微信名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<Python Pyecharts柱状横向动态图>>编写代码&#xff0c;主要功能包括了数据导入&am…

新年新气象----2022年度总结

年度总计——2.0 今年的时间很快飞一般的速度&#xff0c;回顾自己这一年中的日子&#xff0c;不知不觉就结束了&#xff0c;今年确确实实有太多的遗憾&#xff0c;太多的失落和不如意。也让我明白要是这样&#xff0c;就不会那样的道理。 回顾自己去年所定的目标&#xff0c;不…

Spring学习笔记

没有Spring的时候我们如何进行开发的&#xff1f; 没有Spring为我们服务的&#xff0c;写代码一定是这样的 调用层&#xff1a; public static void main(String[] args) {//创建视图层对象TestController testController new TestController();//调用视图层方法testControl…

一篇深入理解MySQL索引底层数据结构与算法

索引的定义 索引是帮助MySQL高效获取数据的排好序的数据结构 索引数据结构有&#xff1a;二叉树&#xff0c;红黑树&#xff0c;Hash表&#xff0c;B-Tree&#xff0c;BTree 对于数据库表的InndB索引来说&#xff0c;底层即是BTree数据结构 B-Tree (1) 叶子节点具有相同的深…

生物化学 实验记录或传闻 简述(暂记)

今天是2023年的1月1日&#xff0c;2022年是疫情肆虐的一年&#xff0c;由于疫情的叨扰&#xff0c;我在22年的最后一个月博客更新停滞了许久&#xff0c;这同时引发了我对一些医学方面的问题的观察&#xff0c;所以我准备在“力学地球物理科学化学”专栏的生物化学部分新增一个…

opencv--颜色识别

目录 一、主要函数介绍 二、颜色识别 三、其他相关函数介绍 1. cv2.resize 2. np.array(x,dtype) 3. cv2.inRange() 4. cv2.imshow() 5.cv2. waitKey() 四、掩膜 一、主要函数介绍 通常&#xff0c;相机在RGB颜色模式下工作&#xff0c;但这种模式受到光线的影响较大&am…

2022年终总结-知识沉淀、疫情、展望未来

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

GIS算法

目录 简介 GIS标准简介 空间数据模型 3.1 WKT与GeoJSON 3.2 WKB格式 GIS算法三方包 简介 地理信息系统(简称GIS),以数字化的形式反映人类社会赖以生存的地球空间的现势和变迁的各种空间数据以及描述这些空间数据特征的属性,支持空间数据及其属性数据的输入/输出,存储,显…

postgresql数据库主备安装,备份还原

一.postgresql数据库安装 1.下载软件包 地址&#xff1a;PostgreSQL: File Browser 2.解压安装 [rootpostgresql u01]# tar -zxf postgresql-14.2.tar.gz安装环境 yum install -y perl-ExtUtils-Embed readline-devel zlib-devel pam-devel libxml2-devel libxslt-devel op…

小说阅读 (支持txt文本文件浏览)

小说阅读2022 - 界面 2023年1月2日 更新 1、修复BUG。。。 2、优化运行。。。 3、添加了 【[阅读笔记] - 可以在章节处填写阅读笔记保存&#xff0c;程序会自动记录位置坐标&#xff0c;每次在读到本章节保存位置坐标时右上角会自动跳出提示图标&#xff0c;点击提示图标即可…

SAP FICO付款及清账

付款及清账 在SAP中&#xff0c;处理付款要达到两个效果&#xff1a; 实现会计凭证的处理&#xff1b; 实现清账&#xff08;将付款与应付账款对应核销&#xff09;&#xff1b; 可以边付边清&#xff0c;也可以先付后清。 一、付款并清账 企业的付款一般有现金付款、银行存款…

Gem5模拟器,全流程运行Chiplet-Gem5-SharedMemory-main(十)

小白入门&#xff0c;跑着个代码头都给我跑秃了&#xff0c;文档说不详细吧似乎啥都和你说了&#xff0c;说它详细吧&#xff0c;看了几遍也跑不出来。记录一下我遇到的问题&#xff1a; 目录 一、确定待执行的程序 &#xff08;1&#xff09;那些年的undefined reference &…

(3分钟速通)Visual Odometry的特征点法和直接法

光流法&#xff1a;(https://zhuanlan.zhihu.com/p/537113460) ​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; ​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; ​ 编辑切换为居中 添加图…

【免费开放源码】审批类小程序项目实战(预约历史页面)

第一节&#xff1a;什么构成了微信小程序、创建一个自己的小程序 第二节&#xff1a;微信开发者工具使用教程 第三节&#xff1a;深入了解并掌握小程序核心组件 第四节&#xff1a;初始化云函数和数据库 第五节&#xff1a;云数据库的增删改查 第六节&#xff1a;项目大纲以及制…

Vue与React分别实现登录拦截【详细例子+讲解】

文章标题一、Vue实现登录拦截1-1、 效果展示1-2 代码展示1-2-1 router.js1-2-2 store文件夹下1-2-2-1 state.js1-2-2-2 index.js1-2-2-3 mutation.js1-2-3 main.js【实现登录拦截的关键---全局路由守卫】1-2-4 pages下的login.vue1-3 总结二、 Vue项目功能简介2-1 功能2-2 查看…

基于RBAC模型的权限控制的一整套基础开发平台,前后端分离,后端django,前端 vue+ElementUI

Django-Vue-Admin 完整代码下载地址&#xff1a;基于RBAC模型的权限控制的一整套基础开发平台 &#x1f4a1; 「关于」 我们是一群热爱代码的青年&#xff0c;在这个炙热的时代下&#xff0c;我们希望静下心来通过Code带来一点我们的色彩和颜色。 因为热爱&#xff0c;所以…