Java后端开发面试题——多线程

news2024/11/27 16:19:00

创建线程的方式有哪些?

继承Thread类

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread...run...");
    }
    
    public static void main(String[] args) {
        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        MyThread t2 = new MyThread() ;
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
    
}

实现runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }
    public static void main(String[] args) {
        // 创建MyRunnable对象
        MyRunnable mr = new MyRunnable();
        // 创建Thread对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

实现Callable接口

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return "ok";
    }
    public static void main(String[] args) throws ExecutionException, 
        InterruptedException {
        // 创建MyCallable对象
        MyCallable mc = new MyCallable() ;
        // 创建FutureTask
        FutureTask<String> ft = new FutureTask<String>(mc) ;
        // 创建Thread对象
        Thread t1 = new Thread(ft) ;
        Thread t2 = new Thread(ft) ;
        // 调用start方法启动线程
        t1.start();
        // 调用ft的get方法获取执行结果
        String result = ft.get();
        // 输出
        System.out.println(result);
    }
}

线程池创建线程

public class MyExecutors implements Runnable{
    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors()) ;
        // 关闭线程池
        threadPool.shutdown();
    }
}

runnable 和 callable 有什么区别?

Runnable 接口run方法没有返回值

Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果

Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

线程的 run()和 start()有什么区别?

start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。

run(): 封装了要被线程执行的代码,可以被调用多次。

线程包括哪些状态,状态之间是如何变化的

public enum State {
    //尚未启动的线程的线程状态
    NEW,
    //可运行线程的线程状态。
    RUNNABLE,
    //线程阻塞等待监视器锁的线程状态。
    BLOCKED,     
    //等待线程的线程状态
    WAITING,
    //具有指定等待时间的等待线程的线程状态
    TIMED_WAITING,
    //已终止线程的线程状态。线程已完成执行
    TERMINATED;
}

新建 T1、T2、T3 三个线程,如何保证它们按顺序执行? 

t.join() 阻塞调用此方法的线程进入timed_waiting 直到线程t执行完成后,此线程再继续执行

Thread t1 = new Thread(() -> {
    System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
    try {
        // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
        t1.join();                          
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
    try {
        // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
        t2.join();                              
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("t3");
}) ;
// 启动线程
t1.start();
t2.start();
t3.start();

notify()和 notifyAll()有什么区别?

notifyAll:唤醒所有wait的线程

notify:只随机唤醒一个 wait 线程

java中wait和sleep方法的不同?

共同点

wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

1.方法归属不同 sleep(long) 是 Thread 的静态方法 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有

2.醒来时机不同 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来 wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去 它们都可以被打断唤醒

3. 锁特性不同(重点) wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制 wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用) 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

如何停止一个正在运行的线程?

使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

使用stop方法强行终止(不推荐,方法已作废)

使用interrupt方法中断线程

        打断阻塞的线程( sleep,wait,join )的线程,线程会抛出InterruptedException异常

         打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字的底层原理

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住

它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor

在monitor内部有三个属性,分别是owner、entrylist、waitset

其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

请谈谈你对 volatile 的理解

①保证线程间的可见性

② 禁止进行指令重排序

        写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下

        读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

volatile int x;
int y;
@Actor
public void actor1() {
    x = 1;
    y = 1;
}
@Actor
public void actor2(II_Result r) {
    r.r1 = y;
    r.r2 = x;
}

        写变量让volatile修饰的变量的在代码最后位置

        读变量让volatile修饰的变量的在代码最开始位置

什么是AQS?

是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的

AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程

在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源

在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性

新的线程与队列中的线程共同来抢资源,是非公平锁 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

ReentrantLock的实现原理

ReentrantLock表示支持重新进入的锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞

ReentrantLock主要利用CAS+AQS队列来实现

支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁

synchronized和Lock有什么区别 ?

语法层面

synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现

Lock 是接口,源码由 jdk 提供,用 java 语言实现

使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

功能层面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量

Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)

性能层面

在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖

在竞争激烈时,Lock 的实现通常会提供更好的性能

死锁产生的条件是什么?

一个线程需要同时获取多把锁,这时就容易发生死锁

如何进行死锁诊断?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack

jps:输出JVM中运行的进程状态信息

jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁      如果有死锁现象,需要查看具体代码分析后,可修复

可视化工具jconsole、VisualVM也可以检查死锁问题

聊一下ConcurrentHashMap

数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表 采用 CAS + Synchronized来保证并发安全进行实现

CAS控制数组节点的添加

synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升

Java程序中怎么保证多线程的执行安全

1.原子性     synchronized、lock

2.内存可见性   volatile、synchronized、lock

3.有序性     volatile

说一下线程池的核心参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

corePoolSize 核心线程数目

maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)

keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

1.AbortPolicy:直接抛出异常,默认策略;

2.CallerRunsPolicy:用调用者所在的线程来执行任务;

3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

4.DiscardPolicy:直接丢弃任务;

线程池中有哪些常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。

2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。

3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的

4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

如何确定核心线程数

IO密集型任务

一般来说:文件读写、DB读写、网络请求等

核心线程数大小设置为2N+1

CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

核心线程数大小设置为N+1

public static void main(String[] args) {
       //查看机器的CPU核数
    System.out.println(Runtime.getRuntime().availableProcessors());
}

参考回答:

① 高并发、任务执行时间短 ( CPU核数+1 ),减少线程上下文的切换

② 并发不高、任务执行时间长

IO密集型的任务  (CPU核数 * 2 + 1)

计算密集型任务 ( CPU核数+1 )

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)

线程池的种类有哪些

 创建使用固定线程数的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

核心线程数与最大线程数一样,没有救急线程

阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

适用于任务量已知,相对耗时的任务

单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

核心线程数和最大线程数都是1

阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

适用于按照顺序执行的任务

可缓存线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

核心线程数为0 最大线程数是Integer.MAX_VALUE 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

适合任务数比较密集,但每个任务执行时间较短的情况

提供了“延迟”和“周期执行”功能的ThreadPoolExecutor

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler);
}

newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行

为什么不建议用Executors创建线程池

  线程池使用场景(CountDownLatch、Future)

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行) 其中构造参数用来初始化等待计数值 await() 用来等待计数归零 countDown() 用来让计数减一

批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM

数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能

异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间

如何控制某个方法允许并发访问线程的数量

// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            // 3. 获取许可
       semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            System.out.println("running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end...");
        } finally {
            // 4. 释放许可
       semaphore.release();
        }
    }).start();
}

Semaphore使用步骤

创建Semaphore对象,可以给一个容量

semaphore.acquire(): 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量) semaphore.release():释放一个信号量,此时信号量个数+1

谈谈你对ThreadLocal的理解

它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享

使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接

set(value) 设置值

get() 获取值

remove() 清除值

ThreadLocal内存泄漏问题

ThreadLocalMap 中的 key 是弱引用,值为强引用;

key 会被GC 释放内存,关联 value 的内存并不会释放。

建议主动 remove 释放 key,value

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

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

相关文章

C++ - 继承

继承的概念 继承机制是面向对象程序设计当中&#xff0c;非常重要且好用的手段&#xff0c;这种手段可以允许程序员在原有的类的特性基础之上&#xff0c;进行拓展从而产生新的类&#xff0c;这些新产生的类&#xff0c;我们成为派生类。在以前&#xff0c;我们实现的复用是函数…

【计算机基础】Git从安装到使用,详细每一步!扩展Github\Gitlab

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

Java网络编程-Socket实现数据通信

文章目录 前言网络编程三要素IP地址和端口号传输协议Socket 使用Scoket实现网络通信TCPTCP通信-发送方TCP通信-接收方结果 UDPUDP通信-发送方UDP通信-接收方结果 总结 前言 本文主要是为下一篇Websockt做铺垫&#xff0c;大家了解socket的一些实现。 网络编程三要素 网络编程…

最小二乘法处理线性回归

最小二乘法是一种数学优化技术&#xff0c;用于查找最适合一组数据点的函数。 该方法主要用于线性回归分析&#xff0c;当然&#xff0c;也可用于非线性问题。 开始之前&#xff0c;我们先理解一下什么是回归。 回归&#xff1a;回归是一种监督学习算法&#xff0c;用于建模和…

中国行政区域带坐标经纬度sql文件及地点获取经纬度方法

文章目录 前言一、如何获取某地的经纬度&#xff1f;1.1 搜索百度地图1.2 在下方找到地图开放平台1.3 下滑找到坐标拾取器1.4 使用 二、sql文件2.1 创建表2.2 插入数据 前言 当工作业务上需要涉及地图&#xff0c;给前端返回经纬度等场景&#xff0c;需要掌握区域经纬度的获取…

Unity中Transform移动相关

路程 方向 * 速度 * 时间 参数一&#xff1a;表示位移多少 路程 方向 * 速度 * 时间 参数二&#xff1a;表示 相对坐标系 默认 该参数 是相对于自己坐标系的 相对于世界坐标系的 Z轴 动 始终是朝 世界坐标系 的 Z轴正方向移动 this.transform.Translate(Vector3.forwar…

经管博士必备基础【12】包络定理

当我们知道一个函数的最优解时&#xff0c;我们要求解这一个函数的值函数关于函数中某一个参数的导数&#xff0c;那么就可以使用包络定理。 1. 无约束条件下的包络定理 函数在其极值点处对一个参数&#xff08;参数不是自变量&#xff09;取偏导数的结果&#xff0c;等价于这…

【vue2第九章】组件化开发和根组件

组件化开发和根组件 什么是组件化开发&#xff1f; 一个页面可以拆分为多个组件&#xff0c;每个组件有自己的样式&#xff0c;结构&#xff0c;行为&#xff0c;组件化开发的好处就是&#xff0c;便于维护&#xff0c;利于重复利用&#xff0c;提升开发的效率。 便于维护&…

openGauss学习笔记-58 openGauss 高级特性-资源池化

文章目录 openGauss学习笔记-58 openGauss 高级特性-资源池化58.1 特性简介58.2 架构介绍58.3 功能特点58.4 适用场景与限制58.5 手动安装示例58.6 OCK RDMA使用示例58.7 OCK SCRLock使用示例 openGauss学习笔记-58 openGauss 高级特性-资源池化 58.1 特性简介 资源池化特性主…

Mybatis-plus使用@TableField(fill = FieldFill.UPDATE)完成自动填充字段如保存更新时自动更新时间

&#x1f4da;目录 填充策略枚举介绍自定义填充策略枚举的数据测试用例插入填充更新填充 结尾 填充策略枚举介绍 使用mybatis-plus完成字段的填充,使用起来也比较方便,当我们往数据库插入一条数据时我们不需要在给指定字段上new Date() ,而是使用mybatis-plus的注解完成TableFi…

VMware 安装 Centos7 超详细过程

CentOS系统&#xff0c;安装教程可参考以下&#xff1a; 哪些模型需要在Linux下运行&#xff0c;需提前预装Linux系统呢&#xff0c;评论区讨论吧 比如Noah-MP 5.0模型 1.软硬件准备 软件&#xff1a;推荐使用 VMware&#xff0c;我用的是 VMware 12 镜像&#xff1a;CentO…

全脑建模:过去、现在和未来

什么是全脑建模&#xff1f; 全脑建模(WBM)是计算神经科学的子领域&#xff0c;涉及近似全脑神经活动的计算和理论模型。该方法的目标是研究神经活动的宏观时空模式如何由解剖连接结构、内在神经动力学和外部扰动(感觉、认知、药理、电磁等)的相互作用产生。这种宏观现象及其模…

在firefox浏览器下破解hackbar

目录 一、介绍&#xff1a; 二、安装教程 1、打开firefox浏览器插件管理扩展 2、在firefox浏览器下安装老版本Hackbar &#xff08;1&#xff09;首先删除之前安装的Hackbar插件&#xff1a; &#xff08;2&#xff09;采用从文件安装附件添加组&#xff1a; &#xff08;3…

Xubuntu16.04系统中解决无法识别exFAT格式的U盘

问题描述 将exFAT格式的U盘插入到Xubuntu16.04系统中&#xff0c;发现系统可以识别到此U盘&#xff0c;但是打不开&#xff0c;查询后发现需要安装exfat-utils库才行。 解决方案&#xff1a; 1.设备有网络的情况下 apt-get install exfat-utils直接安装exfat-utils库即可 2.设备…

Python安装指南(Windows版)

安装python环境 官网下载地址&#xff1a; Download Python | Python.org 我选择3.10.4版本&#xff0c;当然你也可以选择其他版本 安装 安装完成&#xff0c;需要验证是否安装成功。 打开CMD窗口&#xff0c;输入python命令&#xff0c;如果进入如下python窗口则安装成功&…

3D点云处理:栅格化点云(附源码)

文章目录 0. 测试效果1. 基本内容2. 代码实现文章目录:3D视觉个人学习目录微信:dhlddxB站: Non-Stop_目标:将点云进行栅格化操作;应用:一般可应用于点云的体积计算等;0. 测试效果 1. 基本内容 点云栅格化是将三维点云数据投影到二维栅格(或像素)网格上的过程。这种操作…

无涯教程-JavaScript - QUARTILE函数

QUARTILE函数取代了Excel 2010中的QUARTILE.INC函数。 描述 该函数返回数据集的四分位数。四分位数通常用于销售和调查数据中,以将人群分为几类。 语法 QUARTILE (array,quart)争论 Argument描述Required/OptionalArrayThe array or cell range of numeric values for whi…

SpringBoot-学习笔记(基础)

文章目录 1. 概念1.1 SpringBoot快速入门1.2 SpringBoot和Spring对比1.3 pom文件坐标介绍1.4 引导类1.5 修改配置1.6 读取配置1.6.1 读取配置信息1.6.2 读取配置信息并创建类进行封装 1.7 整合第三方技术1.7.1 整合JUnit1.7.1 整合Mybatis1.7.1 整合Mybatis-Plus1.7.1 整合Drui…

stm32HAL库 G4 SPI 从机DMA可变长度接受数据乱码问题

使用一个引脚当作SPI CS, 边沿触发; 在电平位0时候接受SPI数据20个字节 如果实际主机只发了小于20字节时候, 下一个帧就会错乱; 去老外找了之后,需要复位SPi RCC时钟才能复位掉SPI 下面的不行 正确的做法

Redis集群操作-----主从互换

一、将节点cluster1的主节点7000端口的redis关掉 [rootredis-cluster1 src]# ps -ef |grep redis 二、查看集群信息&#xff1a;