多线程之内功精修

news2024/11/16 19:55:32

文章目录

  • 一、常见的锁策略
    • (一)悲观锁和乐观锁
    • (二)读写锁和互斥锁
    • (三)重量级锁和轻量级锁
    • (四)挂起等待锁和自旋锁
    • (五)公平锁和非公平锁
    • (六)可重入锁和不可重入锁
  • 二、CAS
    • (一)概念
    • (二)基于 CAS 实现的操作
      • 基于CAS实现原子类
    • (三)CAS的 ABA 问题
      • 1.概念
      • 2.ABA问题举例说明
      • 3.解决方案
  • 三、Synchronized 中的优化机制(jdk1.8版本机制)
    • (一)Synchronized锁的类型
    • (二)Synchronized的优化手段
      • 1.锁膨胀/锁升级
      • 2.锁粗化
      • 3.锁消除
  • 四、JUC(```java.util.concurrent```)
    • (一)Callable接口
      • 1.用法
      • 2. Callable的理解
      • 3. FutureTask的理解
    • (二)ReentrantLock
      • 1.用法
      • 2.ReentrantLock 和 synchronized 的区别
    • (三)信号量-Semaphore
    • (四)CountDownLatch
  • 五、线程安全的集合类
    • (一)多线程环境使用 ArrayList
    • (二)多线程环境下使用队列
    • (三)多线程环境使用哈希表(==重点==)
      • 1. HashTable
      • 2.ConcurrentHashMap

一、常见的锁策略

锁策略并不仅仅局限于Java,任何关于“锁”的问题,都有可能涉及到锁策略,虽然锁策略往往是给设计锁的人来作为参考的,但是我们了解之后使用起synchronized来也会更加得心应手

(一)悲观锁和乐观锁

1.悲观锁
  在悲观锁看来,它里面的数据会时常发生改变,预期锁冲突的概率很高,因此一个线程拿到锁后,其他线程必须等到锁释放之后才能竞争并访问代码块中的代码。因此我们说悲观锁做的工作多,付出成本高,比较低效

2.乐观锁

  在乐观锁看来,它里面的数据不会经常改变,预期锁冲突的概率很低,因此它允许多个线程同时对数据进行变动,那这样说,锁不就失去作用了吗?并不是,它会对数据是否产生冲突进行检测,如果发现冲突了,就返回错误信息,让用户决定该怎么做。因此我们说,乐观锁做的工作更少,付出成本更低,比较高效

(二)读写锁和互斥锁

  读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。相对于读写锁来说,互斥锁就是普通的锁,只有加锁和解锁操作。

我们需要注意的是:

  • 两个线程都只是读一个数据,此时并没有线程安全问题,直接并发的读取即可
  • 两个线程都要写一个数据,就有线程安全问题
  • 一个线程读另外一个线程写,也有线程安全问题

  因此读写锁就有了下面的特性:一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁

具体操作就是

  • 读加锁和读加锁之间, 不互斥
  • 写加锁和写加锁之间, 互斥
  • 读加锁和写加锁之间, 互斥

读锁:ReentrantReadWriteLock.ReadLock
写锁:ReentrantReadWriteLock.WriteLock

(三)重量级锁和轻量级锁

  前提说明,互斥锁(Mutex Lock)位于操作系统底层,如果频繁的调用互斥锁进行加锁解锁,那么就会导致线程在用户态和内核态之间频繁切换,带来较大的性能损耗

1.重量级锁
加锁机制严重依赖操作系统提供的互斥锁(Mutex   Lock)

  • 频繁用户态内核态切换
  • 容易引发线程调度

以上两个操作成本都比较高

2.轻量级锁
加锁机制尽量不使用mutex,而是尽量在用户态代码完成,实在不行,再使用mutex

  • 少量的用户态内核态切换
  • 不太容易引发线程调度

  通常悲观锁都是重量级锁,乐观锁都是轻量级锁,但是不绝对

(四)挂起等待锁和自旋锁

  1. 挂起等待锁,往往就是通过内核态的一些机制来实现的,往往较重(重量级锁的一种典型实现)
  2. 自旋锁,往往就是通过用户态代码来实现的,往往较轻(轻量级锁的一种典型实现)

  关于以上两种锁,我们可以笼统的认为挂起等待锁就是互斥锁,而自旋锁和互斥锁都是为了解决对某项资源的互斥使用。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,线程进入睡眠状态。但是自旋锁不会引起线程睡眠,而是让线程一直循环看锁是否释放。到时候锁释放后抢占互斥锁的被唤醒,抢占自旋锁的线程直接去抢占自旋锁不需要被唤醒。
  因此才说挂起等待锁是重量级锁,因为它睡眠唤醒的操作就要涉及到内核态了,但是自旋锁只是在用户态让线程不断的观察锁的状态

(五)公平锁和非公平锁

比如说线程A先来,一段时间之后B来了,C在B之后来,不久A把锁释放
对于公平锁和非公平锁而言:

  1. 公平锁遵循“先来后到”,获得锁的顺序就是A->B->C
  2. 非公平锁不遵循“先来后到”,A把锁释放后,B和C等机会进行抢占,谁先抢到算谁的

  对于操作系统来说,在相同优先级的情况下,本身就是随机调度的,因此基于操作系统实现的 mutex 互斥锁,就属于非公平锁,而如果想实现公平锁,反而代价会大一点,比如说起码要有个队列来排先来后到

(六)可重入锁和不可重入锁

  可重入锁的概念与解释我们在多线程基础的时候介绍synchronized时,就已经详细解释了,总的来说,可重入锁就是允许一个线程连续多次获取同一把锁(可以嵌套着使用同一把锁),不可重入锁则表示一个线程只能获取一把锁一次,想要再次获取,必须先释放才能再获取

  Java里只要以 Reentrant 开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括 synchronized 关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁

二、CAS

(一)概念

CAS:全称Compare and swap
一个CAS操作涉及到以下操作:

  1. 比较 A 与 V 是否相等(比较)
  2. 如果相等,将B写入V(交换)
  3. 返回操作是否成功

硬件予以了支持,软件层面才能做到

伪代码说明

//如果A和V表示的是同一块空间的值,那么就可以表示原空间的值不变,就把新值赋值给旧值
while(flg == true) {
    if(A == V) {
        V = B;
        flg = false;
    }
}

  最重要的是,CPU提供了一个单独的CAS指令,换句话说,CAS是原子性的操作,上述伪代码的操作是线程不安全的,但是一条指令是线程安全的,因此CAS最大的意义,就是为我们写线程安全的代码提供了一个新的思路和方向

(二)基于 CAS 实现的操作

基于CAS实现原子类

标准库中提供了java.util.concurrent.atomic包,里面的类都是基于这种方式来实现的
代码示例:

public static void main(String[] args) throws InterruptedException {
    //AtomicInteger就是一个原子类
    AtomicInteger num = new AtomicInteger();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 500; i++) {
              //getAndIncrement方法相当于num++操作
               num.getAndIncrement();
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 500; i++) {
                num.getAndIncrement();
            }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(num);
}

(三)CAS的 ABA 问题

1.概念

  CAS的操作中,第一步是比较,第二步是交换,如果有一个地址,在获取到这个地址的数值之前,数值是1,在比较前数值被其他线程修改,变成0,又变成了1,此时进行比较,1还是1,但已经不是原来的那个1,但是CAS还把他当作原来的那个1来对待,就可能引发一些问题

2.ABA问题举例说明

  假如小明很喜欢小美,并且小明知道小美一直没有男朋友,小美跟小明说,如果三个月之后,我还是单身,我们就在一起。小明答应了,然后这三个月小明和小美再没见过,在这期间,小美换了五个男朋友,等到三个月一到,小美恢复单身,他俩在一起了,虽然小美看起来没什么变化,但是其实她已经不是原来的她了,假如小明有洁癖,他自己是初恋,他也只想跟对方是初恋的情况下谈恋爱,现在暂时没事,但是说不准什么时候小明就知道了,这颗雷就会引爆

3.解决方案

给要修改的值,引入版本号,每次值改变之后,版本号都会加1,在CAS比较数据当前值和旧值的同时,也要比较版本号是否相同

  • CAS操作在读取旧值的同时,也要读取版本号
  • 到了要比较修改的时候
  • 如果当前版本号和读到的版本号相同,就修改数据,并把版本号 + 1
  • 如果当前版本号高于读到的版本号,就操作失败(版本号不同,认为数据已经被修改过了)

三、Synchronized 中的优化机制(jdk1.8版本机制)

(一)Synchronized锁的类型

  1. 即是一个乐观锁,也是一个悲观锁
  2. 不是读写锁
  3. 既是一个轻量级锁,也是一个重量级锁
  4. 轻量级锁的部分是自旋锁的操作,重量级锁的部分是挂起等待锁的操作
  5. 是非公平锁
  6. 是可重入锁

(二)Synchronized的优化手段

1.锁膨胀/锁升级

  JVM将 Synchronized 锁分为无锁,偏向锁,轻量级锁,重量级锁状态,根据情况,依次升级

  1. 无锁,即没有加锁的情况
  2. 偏向锁,有一个线程来加锁,但synchronized不会真的加锁,只是做了一个标记
  3. 轻量级锁(自旋锁),当存在其他线程对这个锁进行竞争时,Synchronized就会转为自旋锁
  4. 重量级锁,当线程很多,竞争很激烈的时候,此时再使用自旋锁对CPU资源浪费就太大了,因此换成重量级锁,即让等待的线程直接挂起等待

2.锁粗化

  粗化对应的就是细化,这里的粗和细是指“锁的粒度

  1. 细指的是一个锁里面代码段少,此时加锁解锁的操作就会更加频繁,但是优点是并发程度高了
  2. 粗是指一个锁里面代码段较多,加锁解锁的频率降低,缺点就是并发程度不高

由此看来,锁粗和锁细各有优缺点
  实际开发中,使用细度锁,是期望释放锁的时候其他线程能够使用锁,但是实际上可能并没有其他线程来抢占这个锁,这种情况JVM就会自动把锁粗化,避免频繁申请释放锁,提高运行效率。但是如果锁比较粗,一般不会进行这种锁优化

3.锁消除

  当一段代码不必加锁(比如单线程环境)而我们给他加了锁,那么就会触发“锁消除”,即把该锁取消掉,是否该取消,由编译器 + JVM来判断

四、JUC(java.util.concurrent)

JUC是处理并发操作的包

(一)Callable接口

1.用法

Callable是一个interface,和Runnable类似,也描述一个任务,但是具有返回值,也是一种创建线程的方式

代码示例(计算1~1000的和并返回):

public static void main(String[] args) {
    Callable<Integer> callable = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for(int i = 1; i <= 1000; i++) {
                sum += i;
            }
            return sum;
        }
    } ;
    FutureTask<Integer> task = new FutureTask<>(callable);
    Thread t = new Thread(task);
    t.start();
    try {
        System.out.println(task.get());
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } catch (ExecutionException e) {
        throw new RuntimeException(e);
    }
}

2. Callable的理解

  1. Callable 和 Runnable 相对,都是描述一个 “任务”。Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务.
  2. Callable 通常需要搭配 FutureTask 来使用。FutureTask 用来保存 Callable 的返回结果。因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。FutureTask就可以负责这个等待结果出来的工作

3. FutureTask的理解

  类似一个转接器,FutureTask实现了Runnable接口,并且内部有一个Callable属性,既然Callable本身无法作为参数传递给Thread,那么就借助FutureTask,实现run方法,在run方法内部执行Callable中的call方法,并保存返回值,最后通过get()来得到返回值
  由于call执行也需要时间,因此get可能会发生阻塞,等待结果的产生

(二)ReentrantLock

  显而易见,它是一个可重入锁,另外,他还是一个互斥锁,,和synchronized类似,都是为了实现互斥,并且能保证线程安全

1.用法

  • lock():加锁,如果获取不到锁就死等
  • trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁
  • unlock():解锁

代码示例:

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

    }finally {
        lock.unlock();
    }
}

  如上图,可以看出ReentrantLock的加锁解锁操作是分开的,这样其实不如合起来,因为很可能代码写的多了会忘记释放锁,因此需要将锁的释放放在finally,这样代码出现问题时,也可以将锁释放,但是写起来比较麻烦

2.ReentrantLock 和 synchronized 的区别

  1. synchronized是一个关键字(逻辑由C++实现),ReentrantLock是一个标准库的类(逻辑由Java代码实现)
  2. synchronized不需要手动释放锁,执行完代码块,锁自动释放,ReentrantLock必须手动释放锁
  3. synchronized如果竞争锁失败,就会阻塞等待,但是ReentrantLock除了阻塞等待操作,还可以指定最长等待时间,失败了直接返回,不必一直死等
  4. synchronized是一个非公平锁,ReentrantLock提供了非公平和公平锁两个选择,如果想要使用公平锁,那么在创建锁时需要传递参数true
  5. synchronized的等待唤醒机制是wait notify,功能比较有限,每次唤醒只是唤醒随机的一个线程;ReentrantLock的等待唤醒机制,是Condition类,功能更加丰富,可以实现指定线程的唤醒操作,这点为第四点的公平锁实现了前提条件

(三)信号量-Semaphore

  信号量,用来表示 “可用资源的个数”。本质上就是一个计数器
  申请一个可用资源,可用资源 - 1(信号量的 P 操作),释放一个可用资源,可用资源 + 1,(信号量的 V 操作),如果计数器的值已经为0,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源
  Semaphore的PV操作都是原子的,可以在多线程环境下直接使用
  信号量可以看作是广义锁,我们理解的锁,是被一个线程获取到之后其他线程就无法获取了,这样的锁又可以被称为“二元信号量”,可用资源就一个,计数器的取值,非0即1

代码示例:

public static void main(String[] args) throws InterruptedException {
    //表示可用资源数为4
    Semaphore semaphore = new Semaphore(4);
    //申请一个资源,可以传参申请多个
    semaphore.acquire();
    System.out.println("申请成功");
    semaphore.acquire();
    System.out.println("申请成功");
    semaphore.acquire();
    System.out.println("申请成功");
    semaphore.acquire();
    System.out.println("申请成功");
    semaphore.acquire();
    System.out.println("申请成功");
    //释放资源
    semaphore.release();
}

  上述代码中,当申请成功四次之后,可用资源数为0,因此就会阻塞,也就不会执行到下面的代码了

(四)CountDownLatch

CountDownLatch是一个线程同步类,能同时等待 N 个任务执行结束

代码示例:

public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++) {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "执行结束");
            //这步操作才是告诉latch,一个线程结束
            latch.countDown();
        });
        t.start();
    }
    //latch中存在类似一个计数器的属性,一个信号发过来,计数器值就减 1
    //如果数目没到之前参数的个数,那么就阻塞等待
    latch.await();
    System.out.println("全部执行完毕");
}

运行结果:
CountDownLatch练习

五、线程安全的集合类

  我们之前学习数据结构时学习到的集合类,大部分都是线程不安全的,有些是线程安全的,但是不建议使用,,因为加锁太多,执行效率太低。比如:

Vector, Stakc,HashTable

(一)多线程环境使用 ArrayList

  1. 自己使用同步机制(synchronized 或者 ReentrantLock)

  2. Collections.synchronizedList(new ArrayList);
    synchronizedList是标准库提供的一个基于synchronized进行线程同步的List
    synchronizedList的关键操作都带有synchronized

  3. 使用 CopyOnWriteArrayList
    CopyOnWrite容器即写时复制的容器。
    当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
    这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
    所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
    优点
     在读多写少的场景下,性能很高,不用加锁
    缺点

    1. 占用内存较多,不适合修改数据量很大的内容,一般可用于更新配置数据
    2. 新写的内存不能被第一时间读取到

(二)多线程环境下使用队列

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列
  2. LinkedBlockingQueue
    基于链表实现的阻塞队列
  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
  4. TransferQueue
    最多只包含一个元素的阻塞队列

(三)多线程环境使用哈希表(重点

  HashMap不是线程安全的,在多线程环境下使用哈希表可以使用:

  • HashTable
  • ConcurrentHashMap

1. HashTable

HashTable只是简单把关键方法,比如get,put加上了synchronized,这相当于直接针对HashTable对象本身加锁,这样的操作会造成极大的锁冲突

  1. 多个线程访问同一个HashTable会冲突
  2. size属性也是通过HashTable来控制同步
  3. 一旦触发扩容,就由当前线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率极低

  我们知道,Java处理哈希冲突的操作是数组 + 链表,因此对于每一个链表来说,它们相互之间是独立的,如果两个操作针对的是不同的链表,那么完全可以对不同链表分开加锁,既保证安全,又大大降低了锁冲突的效率

2.ConcurrentHashMap

ConcurrentHashMap相比于Hashtable做出了一系列的改进和优化,以Java1.8为例

  1. 把锁加到每个链表的头节点上(锁桶)
  2. 只是针对写操作加锁了,读操作没有加锁只使用
  3. 更广泛的使用CAS,比如维护size
  4. 如果扩容,每次操作只搬运一点,通过多次操作完成整个搬运过程,这样做的话,就需要同时维护一个新的HashMap和旧的HashMap,查找的时候既查新的,也查旧的,插入的时候只插新的,直到搬运完毕,再销毁旧的

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

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

相关文章

nerdctl容器管理工具

nerdctl容器管理工具nerdctl简介nerdctl的两个版本安装nerdctl1.配置nerdctl自动补全2.将nerdctl设别名为dockernerdctl使用方法1、运行/计入容器2、容器管理3、镜像管理4、镜像构建nerdctl简介 k8s1.22版本及以上强制安装containerd,要求卸载Docker. 虽然Docker能干的事Conta…

嗨格式数据恢复的 10 种最佳替代方法

当您意识到自己删除了错误的文件时&#xff0c;您是否有过那种恐惧和无助的感觉&#xff1f;或者&#xff0c;也许您的计算机在一项重要任务到期的前一天死机了——您所有的辛勤工作突然消失了。 嗨格式数据恢复是一款流行的数据恢复软件应用程序&#xff0c;它为找回这些文件…

《人月神话》浅读一下吧(上)

1.焦油坑 1.什么是焦油坑 焦油坑是作者用来形容大型系统开发的一个概念。史前时代&#xff0c;恐龙、猛犸象、剑齿虎这些大型食肉动物碰到焦油坑也是没有办法挣脱的&#xff0c;而且越用力就越容易被沉入坑底。 而在项目中好像没有任何一个单独的问题会导致困难&#xff0c;每个…

Spring基础(一)

Spring基础&#xff08;一&#xff09;Spring是什么下载地址IOCAop导入对象创建Spring是什么 Spring是开源的J2EE应用程序框架&#xff0c;针对bean的生命周期进行管理的轻量级容器&#xff0c;其中轻量级是指jar包数量较少。 下载地址 https://repo.spring.io/ui/native/re…

堆与优先级队列

目录 一、堆 1、简介 2、堆的模拟实现 a、向下调整堆 b、向上调整堆 c、插入元素 d、删除堆的根结点 e、获得堆顶元素 二、优先级队列 1、简介 2、常用方法 3、Top-k问题 一、堆 1、简介 堆也是一种数据结构&#xff0c;将一组数据集合按照完全二叉树的方式存储…

C++ 算法进阶系列之从 Brute Force 到 KMP 字符串匹配算法的优化之路

1. 字符串匹配算法 所谓字符串匹配算法&#xff0c;简单地说就是在一个目标字符串中查找是否存在另一个模式字符串。如在字符串 ABCDEFG 中查找是否存在 EF 字符串。 可以把字符串 ABCDEFG 称为原始&#xff08;目标&#xff09;字符串&#xff0c;EF 称为子字符串或模式字符…

Docker搭建SonarQube服务 - Linux

Docker搭建SonarQube服务 - Linux 本文介绍如何在Linux服务器上使用docker简便并快速的搭建SonarQube服务。 参考文档&#xff1a; Prerequisites and Overview&#xff5c;SonarQube Docs Installing SonarQube from the Docker Image | SonarQube Docs 本文使用的镜像版本…

假期来临,Steam内容文件锁定怎么办?

忙忙碌碌又一年&#xff0c;春节假期终于进入倒计时了&#xff01;已经能想象到Steam将迎来一波玩家的狂欢。 不过小编想起不少Windows用户反映过的一个问题&#xff1a;Steam更新游戏时不断收到报错&#xff0c;提示内容文件锁定&#xff0c;怎么办&#xff1f; 为了不妨碍大…

研发与环境的那些事儿

文章目录影响开发效率的环境问题研发需要的环境环境的演变测试单体环境到多环境的演变单体环境上线流程多环境上线流程提供高效研发环境环境是开发工作的核心步骤之一&#xff0c;对研发的开发测试是有影响的。研发与环境之间的关系是非常重要的&#xff0c;研发环境的质量直接…

完美解决了报错:app.js:249 Uncaught TypeError: Cannot redefine property: $router

场景&#xff1a; 项目打包优化阶段&#xff0c;为了解决打包成功后&#xff0c;单文件体积过大的问题 &#xff0c;可以通过 webpack 的 externals 节点&#xff0c;来配置并加载外部的 CDN 资源 原因&#xff1a;报错的原因就是重新定义了$router&#xff0c;因为在项目中安装…

分享134个ASP源码,总有一款适合您

ASP源码 分享134个ASP源码&#xff0c;总有一款适合您 134个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1eZwPKoGGSnzItVBM3_Z77w?pwdxvqz 提取码&#xff1a;xvqz 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&am…

详细搞懂vue2.0 3.0区别-summernote富文本使用

文章目录背景介绍必备知识实操安装回退脚手架vue cli版本vue-使用summernote富文本功能&#xff08;不失效版&#xff09;小知识如果感觉有用点个关注&#xff0c;一键三连吧&#xff01;蟹蟹&#xff01;&#xff01;&#xff01;背景 一开始只是准备实现summernote富文本&…

Stitch it in Time: GAN-Based Facial Editing of Real Videos翻译

代码地址 论文下载 摘要 生成对抗网络在其潜空间内编码丰富语义的能力已被广泛用于面部图像编辑。然而&#xff0c;事实证明&#xff0c;在视频上复制他们的成功具有挑战性。高质量的面部视频集是缺乏的&#xff0c;在视频上存在一个需要克服的基本障碍——时间一致性。我们认…

【Leetcode面试常见题目题解】6. 电话号码的字母组合

题目描述 本文是LC第17题&#xff0c;电话号码的字母组合&#xff0c;题目描述如下&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意…

智能遍历测试在回归测试与健壮性测试的应用

首先来看业界用的较早也是经常听过的一款工具—— Monkey。这是 Android 官方提供的一个工具。谷歌原本设计这款工具是为了对 App 进行压力测试的。谷歌早期在设计 Android 的时候&#xff0c;Android 需要响应滑动、输入、音量、电话等事件&#xff0c;早期 activity 设计不完…

常见的 5 种 消息队列 使用场景

消息队列中间件是分布式系统中重要的组件&#xff0c;主要解决应用耦合&#xff0c;异步消息&#xff0c;流量削锋等问题。 实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性架构。 使用较多的消息队列有 RocketMQ&#xff0c;RabbitMQ&#xff0c;Kafka&#xf…

Android实战场景 - 输入手机号、银行卡号、身份证号时动态格式化

在日常项目开发中&#xff0c;如果稍微严谨点的话&#xff0c;其中关于手机号、银行卡号、身份证号的输入格式有做了限制格式化操作&#xff0c;主要是为了给用户带来更好的体验感&#xff1b; 最近同事正好问到了我这个问题&#xff0c;虽然以前做过这类型功能&#xff0c;但是…

你了解RTK技术吗?—— 揭秘GNSS中的定位技术

上期文章中我们一起探讨了GNSS仿真及其对测试验证的重要意义&#xff0c;今天我们将一起走进GNSS中的定位技术—RTK技术。什么是RTK技术&#xff1f;传统RTK技术与网络RTK技术又有什么区别呢&#xff1f;随着GNSS系统的迅速发展&#xff0c;RTK技术由于可以在作业区域内提供实时…

OpenMLDB v0.7.0 发布

2023 新年伊始&#xff0c;OpenMLDB v0.7.0 正式发布。本次版本更新重点增强了易用性和稳定性&#xff0c;下文将详细介绍主要改进和更新内容。更多 0.7.0 版本内容详见链接&#xff1a;Release v0.7.0 4paradigm/OpenMLDB 系统性改进消息和错误码&#xff0c;提升易用性 在…

【数据库概论】第二章 关系数据库

第二章 关系数据库 目录第二章 关系数据库2.1 关系数据结构2.1.1关系2.1.2关系模式2.1.3关系数据库2.2 关系操作2.2.1 基本的关系操作2.2.2关系数据语言的分类2.3 关系的完整性2.3.1 实体完整性2.3.2 参照完整性2.3.3 用户定义的完整性2.4 关系代数2.4.1 传统集合运算2.4.2 专门…