多线程(5)——锁策略、CAS、JUC常见类

news2024/11/15 17:42:42

1. 常见锁策略

1.1 乐观锁 & 悲观锁

乐观锁 & 悲观锁 也不是指具体某个锁,而是 “锁的一种特点”,描述了 “一类锁”

  • 乐观锁:加锁的时候,假设出现锁冲突的概率不大 => 接下来围绕加锁要做的工作就会更少
  • 悲观锁:加锁的时候,假设出现锁冲突的概率很大 => 接下来围绕加锁要做的工作就会更多

tip:

synchronized 是 “自适应” 的锁,使用 synchronized 时,初始情况下是乐观的(预估接下来锁冲突概率不大),同时会统计锁冲突了多少次,如果发现锁冲突的次数达到了一定程度,就会转变为悲观的

1.2 重量级锁 & 轻量级锁

效果和 乐观锁悲观锁 是重叠的,只是站在的角度不一样

  • 重量级锁,加锁的开销比较大,要做更多的工作(往往悲观的时候会做的重)
  • 轻量级锁,加锁的开销比较小,要做的工作相对较少(往往乐观的时候会做的轻)

tip:

虽然 重量轻量 和 悲观乐观 有重叠,但是不能认为是 100% 等价,乐观悲观是站在 “预估锁冲突” 角度;重量轻量是站在 “加锁开销” 角度

synchronized 也是自适应的

1.3 挂起等待锁 & 自旋锁

  • 挂起等待锁,就是 悲观锁/重量级锁 的一种典型实现
  • 自旋锁,就是 乐观锁/轻量级锁 的一种典型实现

例子:你去追女神,你说:女神我宣你,做我女票吧(你尝试对女神加锁);女神说:你是个好人,我有男票了(女神表示她的锁已经被别的线程加了)

你可以选择 “等待”,每天仍然给女神问候 “早安午安晚安吃了吗...”,这称为 ”自旋锁“,属于忙等,等待的过程中,不会释放 cpu 资源,不停的检测锁是否被释放,一旦释放就立即有机会能够获取到锁(注:这里需要假定锁冲突概率不高的情况下,才能忙等,如果好几个线程都在竞争同一个锁,一个线程拿到锁,其他线程都在忙等,总的 cpu 消耗就会非常高,而且由于竞争太激烈了,就可能导致有些线程要等很久才能拿到锁)

你也可以选择把女神删掉,先不联系了,若干年后听说女神分手了,再去练习,这称为 “挂起等待锁”,不联系相当于 “让出了 cpu 资源”,cpu 就可以用来做其他事情了(挂机等待锁就适合 悲观锁 这样的场景,锁竞争非常激烈,预测拿到锁的概率不大,不妨先把 cpu 让出来去做别的事)

tip:

  • synchronized 是 “自适应” 的,轻量级锁就是基于 自旋 的方式实现的(JVM 内部,用户态代码实现)
  • 重量级锁就是基于 挂起等待 的方式实现的(调用操作系统 api,在内核中实现)

1.4 公平锁 & 非公平锁

假设以下场景:

tip:

  • 在计算机中,约定了 “先来后到” 为公平
  • synchronized 属于 非公平锁,当 N 个线程竞争同一个锁,其中一个线程先拿到了,后续该线程释放锁之后,剩下的 N - 1 个线程需要重新竞争,谁拿到锁都不一定
  • 如果想要使用公平锁,就需要做额外的操作,如:引入队列、记录每个线程加锁的顺序等等...

1.5 可重入锁 & 不可重入锁

死锁问题,如果一个线程针对一把锁连续加锁两次,就可能出现死锁,如果把锁设定为 “可重入”,就可以避免死锁了

原理:

1) 记录当前是哪个线程持有这把锁

2) 在加锁的时候判定,当前申请锁的线程,是否就是锁的持有者线程

3) 计数器,记录加锁的次数,从而确定何时真正释放锁

1.6 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥,而如果两种常见下都用同一个锁,就会产生极大的性能损耗,所有读写锁因此而产生

读写锁(readers-writer lock),在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥

一个线程对于数据的访问,主要存在两种操作:读数据 & 写数据

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

读写锁就是把读操作和写操作区分对待,Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个对象提供了 lock/unlock 方法进行加锁解锁
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象也提供了 lock/unlock 方法进行加锁解锁

其中:

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

tip:

  • 读写锁特别适合 “频繁读,不频繁写” 的场景中
  • synchronized 不是读写锁

1.7 synchronized 的加锁过程

当代码执行到 synchronized 代码块中,JVM 大概要做的事情:

锁升级的过程:刚开始使用 synchronized 加锁,锁会处于 “偏向锁” 状态,遇到线程之间的锁竞争,会升级到 “轻量级锁”,进一步统计锁竞争出现的频次,达到一定程度后,升级到 “重量级锁”

偏向锁:

偏向锁不是真的加锁(真的加锁开销可能会比较大),只是做个标记(标记的过程非常轻量高效)

对于当前 JVM 的实现来说,上述锁升级的过程属于 “不可逆” 的

1.8 锁消除 & 锁粗化

二者都是编译器的优化策略

锁消除:编译器会对 synchronized diamagnetic做出判定,判定这个地方是否确实需要加锁,如果这里没必要加锁的话,就能自动把 synchronized 给优化掉

锁粗化:

2. CAS

2.1 概念

CAS:全称 Compare and swap,字面意思 “比较并交换”

比较 内存 和 cpu 寄存器中的内容,如果发现相同,就进行交换(交换的是 内存 和 另一个寄存器 的内容),具体操作如下:

一个内存的数据和两个寄存器中的数据进行操作(寄存器 1 和寄存器 2)

1) 比较 内存 和 寄存器 1 中的值是否相等

2) 如果不相等,无事发生;如果相等,就交换 内存 和 寄存器 2 的值

3)返回操作是否成功

CAS 通过 “一个 cpu 指令” 完成了上述一系列的操作(原子的),因此 CAS 就可以给编写多线程代码带来新思路:“无锁化编程”

2.2 CAS 具体的使用场景

2.2.1 基于 CAS 实现 “原子类”

int / long 类型在进行 ++ -- 操作时,都不是原子的,基于 CAS 实现的原子类,对 int / long 等这些类型进行了封装,从而可以原子的完成 ++ -- 等操作

原子类在 Java 标准库中,也有实现:

import java.util.concurrent.atomic.AtomicInteger;

public class Demo34 {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement(); //count++
                count.incrementAndGet(); //++count
                count.getAndDecrement(); //count--
                count.decrementAndGet(); //--count
                count.getAndAdd(10); //count += 10
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement(); //count++
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        //通过 count.get() 拿到原子类内部持有的真实数据
        System.out.println("count = " + count.get());
    }
}

如何通过 CAS 实现的原子类(下面用一段伪代码来理解):

画图理解:

2.2.2 实现自旋锁

基于 CAS 实现更灵活的锁,获取到更多的控制权

自旋锁伪代码:

2.3 CAS 的 ABA 问题

CAS 之所以能保证线程安全,是因为在通过 CAS 比较的过程中来确认当前是否有其他线程插入进来执行

此处是通过判定值是否相同,来区分是否有其他线程修改过

但是 值相同 不等于 没有修改过,因为有可能有另一个线程修改了,又修改回去了

CAS 中确实存在 ABA 问题,但是大多数情况,ABA 问题并不会带来 bug


也有非常极端的场景:ATM 的转账功能,转账过程中通过 CAS 的方式来实现:

解决上述问题的方案:引入版本号

因为 “余额” 能加也能减,所以才会有 ABA 问题,所以引入版本号,是一个整数,设定为只能增加,如下:

3. JUC(java.util.concurrent)的常见类

3.1 Callable 接口

Callable 类似于 Rannable,只不过 Callable 中的 call 方法带有返回值,而 Rannable 中的 run 方法不带返回值(void),示例如下:

创建一个线程,让这个线程计算 1 + 2 + 3 +...+ 1000

public class Demo35 {
    private static int result;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i < 1000; i++) {
                sum += i;
            }
            result = sum;
        });

        t.start();
        t.join();

        System.out.println("result = " + result);
    }
}

该方法要想得到结果,必须引入一个成员变量,相当于引入了一个额外的依赖,让线程和成员变量及主线程和成员变量之间的产生了明显的耦合,虽然可以满足需求,但并不是一个很好的选择

而 Callable 就是解决上述问题的,如下:

完整代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo36 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i < 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        //后续需要通过 FutureTask 拿到最终的结果
        System.out.println(futureTask.get());
    }
}

总结:创建线程的方式

1. 直接继承 Thread

2. 使用 Runnable

3. 使用 Callable

4. 使用 lambda

5. 使用线程池

3.2 ReentrantLock

可重入互斥锁,和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全

synchronized 只是 Java 提供的一种加锁的方式

而 ReentarantLock 是一种经典风格的锁,通过 lock 和 unlock 方法来完成加锁解锁的


ReentrantLock 和 synchronized 的区别

1) synchronized 属于关键字(底层是通过 JVM 的 C++ 代码实现的);ReentrantLock 则是标准库提供的类,通过 Java 代码来实现的

2) synchronized 通过代码块控制加锁解锁;ReentrantLock 通过调用 lock、unlock 方法来完成(需将 unlock 放到 finally 中,防止意外执行不到)

3) RenntrantLock 提供了 tryLock 这样的加锁风格,前面学习的锁都是发现锁被别人占用了,就阻塞等待;tryLock 在加锁失败的时候,不会阻塞,而是直接返回,通过返回值来反馈是加锁成功还是失败

4) ReentrantLock 还提供了 公平锁 的实现(默认是非公平的,可以在构造方法中传入参数,设定为公平的)

5) ReentrantLock 还提供了功能更强的 “等待通知机制”,基于 Condition 类,能力比 wait、notify 更强一些

代码示例:

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class Demo37 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock locker = new ReentrantLock(true);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                count++;
                locker.unlock();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                count++;
                locker.unlock();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " +count);
    }
}

3.3 信息量 Semaphone

信息量,用来表示“可用资源的个数”,本质上就是一个计数器

(acquire)申请资源,让计数器 - 1,也成为“P 操作”;(release)释放资源,让计数器 + 1,也成为“V 操作”,如果计数器为 0,继续申请就会出现阻塞

操作系统本身提供了信号量实现,JVM 将操作系统的信号量封装了以下,我们可以直接用:

代码示例:

import java.util.concurrent.Semaphore;

public class Demo38 {
    public static void main(String[] args) throws InterruptedException {
        //可用资源的个数,计数器的初始值
        Semaphore semaphore = new Semaphore(3);
        
        semaphore.acquire();
        System.out.println("申请一个资源!");
        semaphore.acquire();
        System.out.println("申请一个资源!");
        semaphore.acquire();
        System.out.println("申请一个资源!"); //若连续申请 4 个资源,到这里就会阻塞等待
        
        semaphore.release();
        System.out.println("释放一个资源!"); //释放一个资源之后,才能申请下一个资源
        
        semaphore.acquire();
        System.out.println("申请一个资源!");
    }
}

使用信息量改进 Demo37:

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class Demo37 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " +count);
    }
}

3.4 CountDownLatch

同时等待 N 个任务执行结束(类似于跑步比赛,10个选手依次就位,哨响同时出发,所有选手都通过中终点才公布成绩)

很多时候,需要把一个大的任务拆成多个小任务,通过多线程/线程池执行,如何衡量所有的任务都执行完毕呢?

比如:多线程下载

浏览器的下载一般是单线程的,下载速度是有限的(2~3MB/秒),但是可以通过多线程的方式提高下载速度,使用专门的下载工具,通过多个线程和服务器建立多个网络连接(服务器进行网速限制是针对一个连接做出的限制),如果创建出 10~20 个线程,下载的总速度就能大幅提高

多个线程,每个线程下载一部分,所有线程下载完毕再进行拼装

代码示例:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo39 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        //构造方法的数字,就是拆分出来的任务个数
        CountDownLatch countDownLatch = new CountDownLatch(20);

        for (int i = 0; i < 20; i++) {
            int id = i;
            executorService.submit(() -> {
                System.out.println("下载任务 " + id + " 开始执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("下载任务 " + id + " 结束执行");
                //执行完毕
                countDownLatch.countDown();
            });
        }
        //当 countDownLatch 收到了 20 个“完成”,所有的任务就都完成了
        //await => all wait
        //await 这个词也是计算机术语,在 python/js 中意思是 async wait(异步等待)
        countDownLatch.await();

        System.out.println("所有任务都完成!");
    }
}

3.5 线程安全的集合类

ArrayList、Queue、HashMap...都是线程不安全的

Vector、Stack、Hashtable,虽然是线程安全的(内置了 synchronized),实际上并不推荐使用

因为 sunchronized 会造成线程阻塞,如果本来就没有线程安全问题,却使用了这些集合类,会造成程序效率大打折扣

3.5.1 解决方案:

1) 自己加锁

2) 如果需要使用 ArrayList / LinkedList 这样的结构,标准库中提供了一个带锁的 List

Collections.synchronizedList(new ArrayList);

相当于构造出一个核心方法自带 synchronized 的 List


还可以使用 CopyOnWrite 集合类

CopyOnWriteArrayList

这个集合类没有加锁,是通过 “写时拷贝” 来实现线程安全的,如下:

这个赋值引用的操作,本身是原子的,即使在这个过程中存在大量的读操作来读取内容,仍然能确保读到的数据是有效数据(读取的数据要么是旧版本数据,要么是新版本数据,不会是一个 “修改了一半” 的数据)

如果修改操作直接基于旧版本来修改,同时还有其他线程去读取,就容易读到 “修改了一半” 的数据(ArrayList 有的修改是原子的,也有一些修改不是原子的,如:插入/删除操作)

如果是针对多个线程同时写的情况,写时拷贝机制就难以应付了,因此当前的写时拷贝机制,主要是用来应对 “多个线程读,一个线程写” 这样的场景

实例场景:广告服务器

在广告服务器运行过程中是涉及到很多 “配置” 的,这些配置都是写到一个 “配置文件” 中,服务器运行的时候,加载配置文件中的配置项,根据配置项来启用/关闭/设置某些功能

如果在服务器运行过程中就想修改配置(地域投放 -> 设为关闭),只该文件是不够的,还需要让服务器重新加载,就需要 “重启”,如果不想重启,就可以通过 写时拷贝 这样的机制来实现

3) 想多线程环境下使用 队列:BlockingQueue


4) 多线程环境下使用 哈希表

Hashtable 虽然是可选项,但是更推荐 ConcurrentHashMap,这个数据结构相比于 HashMap 和 Hashtable 来说,改进力度非常大,改进如下:(高频面试题)

a) 优化了锁的粒度(最核心)

Hashtable 的加锁就是直接给 put、get 等方法加上 synchronized,就是给 this 加锁

整个 哈希表 对象就是一把锁,任何一个针对这个哈希表的操作都会触发锁竞争

而ConcurrentHashMap 则是给每个 hash 表中的 “链表” 进行加锁(不是一把锁,而是多把锁),称为 “锁桶”

上述设定方式是可以保证线程安全的,其次这个设定方式大大降低了锁冲突的概率,只有同时进行的两次修改恰好在修改同一个链表上的元素时,才会触发锁竞争(一个 hash 表上有很多链表)

b) ConcurrentHashMap 引入了 CAS 原子操作

针对像修改 size 这样的操作,直接借助 CAS 完成,并不会加锁

c) 针对读操作,做了特殊处理

上述的加锁只是针对写操作,对于读操作,通过 volatile 以及一些精巧的代码实现,确保读操作不会读到 “修改了一半的数据”

d) 针对 hash 表的扩容进行了特殊的优化

普通 hash 表扩容,需要创建新的 hash 表,把元素都搬运过去,这一系列操作很有可能在依次 put 就完成了,这就会使这次 put 的开销非常大,耗时非常长

ConcurrentHashMap 进行了 “化整为零”,不会在一次操作中进行所有数据的搬运,而是一次只搬运一部分,后续的每次操作都会触发一部分 key 的搬运,最终把所有的 key 都搬运完

当新旧同时存在的时候

1) 插入操作直接插入到新的空间中

2) 查询/修改/删除,都是需要同时查询旧的空间和新的空间

tip:分段锁

分段锁是 ConcurrentHashMap 早期的实现方式,和现在锁桶的思想是一样的,实现上有差别:

将所有的同分段,每一段有一个锁(一个锁管多个链表),缺点:

a) 这里降低锁冲突做的不够彻底

b) 分段锁的实现方式更复杂

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

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

相关文章

minio 后端大文件分片上传,合并,删除分片

背景 网上大多数minio大文件上传都是采用后台返回前端预上传链接&#xff0c;然后由前端去put请求直接和minio通信上传分片文件&#xff0c;然后调用后台合并分片逻辑来达到快申诉上传的目的&#xff0c;详情可以参考我的上两篇文章 最近有个项目域名是https的&#xff0c;但…

# 低代码和无代码开发初探

低代码和无代码开发初探 低代码和无代码都是近年来在软件开发领域兴起的技术趋势&#xff0c;它们旨在提高开发效率、降低开发门槛&#xff0c;让更多人能够参与到软件开发过程中。以下是对低代码和无代码的介绍&#xff1a; 一、低代码 1、低代码定义 低代码开发平台&…

Linux nice/renice 命令 - 进程的NI、PRI属性

进程NI、PRI属性的联系 共同影响进程调度&#xff1a;NI和PRI都是Linux进程调度机制中的重要参数&#xff0c;它们共同决定了进程在CPU资源竞争中的优先级。NI值通过影响PRI值来间接影响进程的调度顺序。NI值可调整以改变PRI值&#xff1a;用户可以通过调整进程的NI值来间接改…

较难!第15届蓝桥杯青少组省赛Scratch中级组编程真题

今天上午第15届蓝桥杯青少组省赛Scratch初级组考完试以后&#xff0c;Scratch实验室就预估今天下午的Scratch中级组比较难&#xff0c;结果不出所料&#xff0c;还是比较有难度&#xff0c;据好几个学生及家长说&#xff0c;好几道题不会做时间不够。 来源&#xff1a;结束啦&a…

三级_网络技术_43_综合题(报文)

一、 某客户机使用DHCP获取IP地址等信息&#xff0c;其获取lP地址过程中捕获的4条报文及对第2条报文分析如下所示。请分析其中的信息&#xff0c;补全内容。 编号 报文摘要 DHCP:Request, Type:DHCP discover DHCP:Reply, Type:DHCP__________ DHCP:Request, Type:DHCP Re…

DBeaver安装使用

文章目录 简介支持的数据库支持的系统 下载安装DBeaver使用修改Maven下载jar地址窗口->首选项连接->驱动->Maven配置仓库地址 选择需要连接的数据库进行连接 简介 DBeaver 是一个通用的数据库管理工具和 SQL 客户端&#xff0c;支持 MySQL, PostgreSQL, Oracle, DB2,…

人生苦短,转行程序员要趁早啊

前言 最近有朋友咨询关于如何自学编程语言的问题&#xff0c;发现要回答这个问题&#xff0c;不是一俩句就可以回答清楚并减少当事人的困惑和迷茫。 笔者不知道提问者是问的学习方法还是学习路径&#xff0c;所以特此写一篇文章&#xff0c;斗胆表达一下我对自学编程的一点点…

解决Gradle下载依赖速度慢的问题

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

盘古信息MES制造执行系统,赋能制造企业智能化运营管理

随着工业智能化的不断深入&#xff0c;MES系统在制造业中扮演着越来越重要的角色。盘古信息自主研发的IMS MES&#xff0c;通过提供包括制造数据管理、计划排程管理、生产调度管理、库存管理、质量管理、人力资源管理、设备管理、采购管理、成本管理、看板管理、生产过程控制、…

TCP粘包和抓包

在 TCP 套接字中&#xff0c;发送和接收缓冲区用于暂存数据&#xff0c;以确保数据的可靠传输。具体来说&#xff0c;TCP 的 socket 收发缓冲区的主要特点和概念如下&#xff1a; 1. 发送缓冲区&#xff08;Send Buffer&#xff09; 定义: 发送缓冲区用于存储待发送的数据。应…

如何在不格式化的情况下解锁 Android 智能手机密码

如果您忘记密码&#xff0c;您的 Android 移动设备将锁定您。发生这种情况时&#xff0c;通常可以通过恢复出厂设置来重新获得对设备的访问权限。可悲的是&#xff0c;这将导致所有数据丢失。下面列出的是解锁锁定的Android 手机而不会丢失任何个人数据的有效方法。 Android 手…

排查Maven问题的步骤

0.检查pom文件完整性 1.检查IDEA中配置是否正确 2.使用清楚工具将所有的lastupdate清除, 3.有些依赖是公司的依赖 —>配置私服 —>拷贝同事仓库,覆盖自己的仓库 4.有了私服地址,但是还是下载不到 —>查看地址是否能访问 —>挂VPN

linux多进程与多线程总结

这里写自定义目录标题 2 linux多进程与多线程2.1 进程间通信2.1.1 管道2.1.2 信号2.1.3 消息队列2.1.4 共享内存 3 线程4 IO多路复用4.1 非阻塞IO4.2 IO多路复用 2 linux多进程与多线程 学习并发程序。 linux系统中&#xff0c;使用树型管理进程。因此进程之间有父子关系。通…

如何使用ssm实现学生公寓管理系统的设计与实现

TOC ssm106学生公寓管理系统的设计与实现jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;…

LeetCode.3146.两个字符串的排列差

题目描述&#xff1a; 给你两个字符串 s 和 t&#xff0c;每个字符串中的字符都不重复&#xff0c;且 t 是 s 的一个排列。 排列差 定义为 s 和 t 中每个字符在两个字符串中位置的绝对差值之和。 返回 s 和 t 之间的 排列差 输入输出示例&#xff1a; 思路一&#xff1…

TMC2209模块开启无限位归零

TMC2209无限位归零配置步骤&#xff1a; 3.1 配置内部采样电阻。具体在GCONF中internal_Rsens 1&#xff1b; 此步发送数据 05 00 80 00 00 00 83 00 3.2 电机电流设置&#xff0c;配置IHOLD_RUN寄存器&#xff1b; 此处发送数据 05 00 90 00 00 16 16 12 3.3 设置失速电流阈…

QT接收并解析GPS模块串口数据

目录 一、QT读取串口数据 二、解析数据 目标&#xff1a; 使用QT&#xff0c;读取gps模块的串口数据&#xff0c;并解析其中的经纬高数据&#xff0c;然后进行处理 一、QT读取串口数据 变量定义 QSerialPort *serial; QSerialPortInfo SerialPortInfo; QByteArray lineData…

Deepin【2】:Deepin系统盘扩容

Deepin【2】&#xff1a;Deepin系统盘扩容 1、进入live系统1.1、live系统入步骤 2、连接网络3、新增系统仓库4、安装gparted应用5、使用gparted进行扩容操作5.1、观察当前分区5.2、压缩data分区5.3、Rootb分区合并空闲空间5.4、Rootb分区压缩空间5.5、Roota合并空闲空间5.6、核…

【学习笔记】STM32F407探索者HAL库开发(二)STM32F4最小系统设计

【学习笔记】STM32F407探索者HAL库开发&#xff08;二&#xff09;STM32F4最小系统设计 0 什么是最小系统1 电源电路1.1 数字部分电源1.2 模拟部分电源1.3 参考电压1.4 VBAT 2 复位电路3 BOOT启动电路&#xff08;F4&#xff09;4 晶振电路4.1 低速晶振4.2 高速晶振 5 下载调试…

计算机操作员试题(公共科目)

计算机操作员试题(公共科目) ★★★ RZer整理 仅供参考 ★★★ 单项选择题 “人的本质”这一概念是人对自身的认识发展到一定阶段形成的,它力图从根本上回答“人是什么”或“什么是人”这一重大问题。马克思主义认为,人的本质( )。 A.永恒不变 B.可随主观意志而任意改变 C…