多线程-线程安全

目录

线程安全问题

加锁(synchronized)

synchronized 使用方法

synchronized的其他使用方法

synchronized 重要特性(可重入的)

死锁的问题

对 2> 提出问题

对 3> 提出问题

 解决死锁

对 2> 进行解答

对4> 进行解答

volatile 关键字

wait 和 notify (重要)

wait使用实例:

notify使用实例:

" 线程饿死 "

notify 和 notifyAll

小结


线程安全问题

线程安全问题: 有些代码在单个线程环境下执行完全正确. 但是如果同样的代码让多个线程同时执行, 此时就可能出现 bug, 这种情况叫做 "线程安全问题" / "线程不安全", 它是多线程中最复杂, 最重要的部分

举个例子: 两个线程, 每个线程count++ 5000次, 正常情况下结果为 10w, 实际结果:如下图:

public class Test {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {int i = 0;while(i < 5000) {count++;i++;}});Thread t2 = new Thread(() -> {int i = 0;while(i < 5000) {count++;i++;}});t1.start();t2.start();//如果没有这俩 join , 肯定不行, 线程还没有自增完毕, 就开始打印了,//打印出来的count 可能是 0;t1.join();t2.join();//预期结果是 10wSystem.out.println(count);}
}

改变一下 join 的次序可以让结果输出正确, 如下代码:

public class Test {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {int i = 0;while(i < 5000) {count++;i++;}});Thread t2 = new Thread(() -> {int i = 0;while(i < 5000) {count++;i++;}});t1.start();t1.join();t2.start();t2.join();//预期结果是 10wSystem.out.println(count);}
}

这个代码意味着 t1 执行时 t2并不会启动, 虽然上述代码卸载两个线程中, 但并不是同时执行的, 而第一个代码中 t1 和 t2 同时执行了, 第二个代码结果输出正确, 为 10w, 我们可以猜测是因为两个线程同时执行的原因导致第一个结果出错了.

解释:

count++ 这个操作本质上是分三步进行的 ~~ 站在 cpu 的角度, 是 cpu 通过三个指令实现的

1> load 把数据从内存读到 CPU 寄存器中

2> add 把寄存器中的数据进行 +1

3> sava 把寄存器中的数据保存到内存中

由于多个线程执行上述代码, 由于线程之间的调度顺序是 "随机" 的, 就会导致有些调度顺序下, 上述的逻辑就会出现问题.

如图:

这只是其中的一种情况, 还可能有无数种情况, 这三个步骤的排列顺序有很多种了, 还有可能 t1 连续执行了多次, 然后 t2 再次执行的情况, 有无数种排列顺序.

我们意识到在多线程程序中最困难的一点是: 现成的随机调度, 是两个线程执行逻辑的先后顺序存在很多可能, 我们要做的是保证在每一种情况下都输出正确的结果.

举个例子看一下: 不同的情况怎么输出结果的:

理想情况下:

两次相加后, 最终可以输出2

可能会出现的情况:

这种情况下两次相加得到的结果为 1

因为线程调度是随机的, 很容易出现错误情况, 这样的话最终的结果是一个随机值, 随机值小于 10w.

产生线程安全的原因:

1> 操作系统中, 线程的调度顺序是随机的 (抢占式执行)

2> 两个线程针对同一个变量进行修改

3> 修改操作不是原子的

此处给定的 count++ 就属于是非原子的操作, 先读取, 在修改, 有三个指令

4> 内存可见性问题

5> 指令重排序问题

 如何解决这个问题呢? 从这些原因入手

1> 调度随机性在系统内核里实现的, 最早的操作系统奠定了这个基调, 无能为力.

2> 有些情况可以通过调整代码结构来规避在这个问题, 有些情况规避不了

3> 有办法让 count++ 三步走成为 "原子" 的   ---->  (加锁) 的方法

加锁(synchronized)

synchronized 使用方法

需要搭配一个代码块 {   } 使用, 进入  {  就会加锁, 出去  }  就会解锁

作用

在已经加锁的状态下, 另一个线程尝试同样加这个锁, 就会产生 "锁冲突/锁竞争", 后一个线程就会阻塞等待, 一直等到前一个线程解锁为止.

使用方法举例

用上述代码进行举例:

count++ 加在代码块中, 然后 synchronized()  这个后面的 () 需要表示一个用来加锁的对象, 这个对象是啥不重要, 重要的是通过这个对象来区分两个线程是否在竞争同一个锁, 如果两个线程是针对同一个对象加锁, 就会有锁竞争, 反之不会有锁竞争, 仍然是并发执行.

追妹子: 你想妹子表白, 成功了就相当于加锁了, 另一个小哥准备追同一个妹子, 就得阻塞等待, 等你俩分手了他才有机会, 如果他准备追另一个对象, 那么可以直接表白.

public class Test {public static int count = 0;public static void main(String[] args) throws InterruptedException {//我们任意定义一把锁Object locker = new Object();Thread t1 = new Thread(() -> {int i = 0;while(i < 5000) {//进行加锁synchronized (locker) {  count++;i++;}}});Thread t2 = new Thread(() -> {int i = 0;while(i < 5000) {//进行加锁synchronized (locker) {count++;i++;}}});t1.start();t2.start();t1.join();t2.join();//预期结果是 10wSystem.out.println(count);}
}

 运行结果正确了.

加过锁之后两个线程相互影响, 在进行 count++ 时会先加锁, t1 线程加过锁了, t1没执行完之前, t2 操作会出现阻塞. 只有当 t1 中 count++ 的操作执行完之后才会让 t2 中的 count++ 进行操作, 这就避免了 t1 中的 load add save 与 t2 中的 load add save 操作 出行穿插, 此时线程安全问题就迎刃而解了.

如果在 两个线程加锁时 使用不同的 锁 那么就不会出现锁竞争, 上述问题就不会解决

其中synchronized 后面 () 中的锁对象到底是哪个对象无所谓, 重要的是俩线程加锁的对象是否是同一个对象.

synchronized的其他使用方法

synchronized 还可以修饰 一个方法

class Counter {public int count;synchronized public void increase() {count++;}public void increase2() {synchronized (this) {count++;}}
}
public class Test {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();//预期结果是 10wSystem.out.println(counter.count);}
}

两种的写法是一样的, 上面是下面的简化版本

synchronized还可以修饰一个 静态方法

两种的写法是一样的, 上面代码是下面代码的简化版本

其中 Counter.class 是类对象

.java => .class => JVA加载到内存中(类对象) 可以看作是 .Java 文件中的二进制码

类对象中包含以下内容: 

1> 类的属性有哪些, 名字, 类型, 权限

2> 类的方法有哪些, 名字, 类型, 权限

3> 类本身继承自哪个类, 实现了哪些接口

在一个 Java 进程中, 类对象是唯一的.

synchronized 用的锁是存在 Java 对象头里的

Java 的一个对象, 对应的内存空间中, 除了你自己定义的一些属性之外, 还有一些自带的属性, 在对象头中, 其中就有属性表示当前的对象是否已经加锁.

synchronized 重要特性(可重入的)

可重入定义: 一个线程连续针对一把锁加锁两次, 不会出现死锁, 满足这个要求就是 "可重入" , 不满足就是 "不可重入" .

 死锁的解释: 有一个线程 t , 锁对象 locker,  t 线程中存在下列代码:

synchronized (locker) {

          synchronized (locker) {

                 ........... 

         }

}

第一次加锁能够加锁成功, 此时 locker 属于 "被锁定" 的状态, 第二次加锁 locker 已经是锁定状态, 第二次加锁操作, 应该要 "阻塞等待" 的, 等到锁被释放之后才能加锁成功

第二次想要加锁成功, 需要第一次加锁释放锁, 释放锁就要第二次加锁成功

这样就出现了死锁现象. 就是一个bug, 可能会出现这种情况

    private static Object locker = new Object();public static void func1() {synchronized (locker) {func2();}}public static void func2() {func3();}public static void func3() {func1();}public static void func4() {synchronized (locker) {}}

 这种bug时常出现而且不容易发现.

问题是: 上述代码中, synchronized 是可重入锁, 没有因为第二次加锁而死锁, 加入上述加锁过程有 N 层, 释放时机该如何判定? 

解答: 此处无论有多少层锁, 都是到在最外层才能释放锁, 提前释放会线程不安全

引用计数: 锁对象中不但要记录谁拿到了锁, 还要记录锁被加了几次, 每加锁一次, 计数器 +1, 每解锁一次, 计数器 -1, 除了最后一个大括号恰好减成 0 , 才真正释放锁.

死锁的问题

关于死锁总结: 

1> 一个线程针对一把锁, 连续加锁两次, 如果是不可重入锁, 就死锁了. (synchronized 不会出现)

2> 两个线程, 两把锁 (此时无论是不是可重入锁, 都会死锁)

 t1  t2 两个线程,  A 和 B 两把锁

t1 获取锁 A, t2 获取锁 B,  t1 尝试获取B, t2 尝试获取 A. 这种情况出现死锁.

3> N 个线程, M 把锁 (相当于 2> 的扩充) 更容易出现死锁的情况了, 经典模型: 哲学家就餐问题.

对 2> 提出问题

public class Test {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (locker1) {// 此处的 sleep 很重要, 要确保 t1 和 t2 都分别拿到一把锁// 之后再进行动作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t1 加锁成功!");}}});Thread t2 = new Thread(() -> {synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1) {System.out.println("t2 加锁成功!");}}});t1.start();t2.start();}
}

此时的代码会出现死锁情况, 什么都没法打印出来

 此时打开 jconsole (java带的一个查看线程情况的工具) 就可以看到这俩线程的状态:

当前线程出现了阻塞状态;

对 3> 提出问题

死锁是属于很严重的 bug (导致线程卡住, 无法执行后续工作)

死锁的成因涉及到四个 必要条件

1> 互斥使用 (锁的基本特性) 当一个线程持有一把锁之后, 另一个线程也想获取到锁, 就要阻塞等待

2> 不可抢占 (锁的基本特性) 当锁已经被线程 1 得到之后, 线程2 只能等线程1 主动释放出来, 不能强行抢过来

3> 请求保持 (代码结构) 一个线程尝试获取多把锁, (先拿到锁1 之后, 在尝试获取锁2 的时候锁1 不会释放, 就是的上面的2> 例子

4> 循环等待/环路等待  (代码结构) 等待的依赖关系形成环了, 上面的3> 哲学家就餐的例子

 解决死锁

如何解决/避免死锁呢?

核心是破坏上述必要条件

1> 和 2> 是锁的特性, 不能改变, 要从 3> 和 4> 着手

3> 来说, 调整代码结构, 避免编写 "锁嵌套" 逻辑,  当然这个方案不一定好使, 有的需求可能就是需要获取多个锁之后再操作

4> 通过约定加锁顺序, 就可以避免循环的等待

对 2> 进行解答

调整代码结构, 避免编写 "锁嵌套" 逻辑

public class Test {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (locker1) {// 此处的 sleep 很重要, 要确保 t1 和 t2 都分别拿到一把锁// 之后再进行动作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (locker2) {System.out.println("t1 加锁成功!");}});Thread t2 = new Thread(() -> {synchronized (locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (locker1) {System.out.println("t2 加锁成功!");}});t1.start();t2.start();}
}

这样就能正常输出结果了.

对4> 进行解答

通过约定加锁顺序, 就可以避免循环的等待

针对锁进行编号, 比如约定 加多把锁的时候先加编号小的锁, 后加编号大的锁.(所有线程都遵守这个规则) 举个例子如下图:

最终死锁问题迎刃而解, 本质上是破除了循环等待

synchronized 使用规则并不复杂, 抓住一个原则, 两个线程针对同一个对象加锁, 就会产生锁竞争.

volatile 关键字

作用: 

1> 保证内存可见性

2> 禁止指令重排序

1>什么是 内存可见性

计算机运行的程序/代码, 经常要访问数据, 这些依赖的数据往往存在在 内存中, (定义一个变量, 变量就是存在内存中), CPU 使用这个变量的时候, 就会把这个内存中的数据先读出来, 放到 CPU 的寄存器中在参与运算 (load)

CPU 读内存 相当于 读硬盘 快几千上万倍, 读寄存器 相比于 读内存 又快了几千上万倍, 为了提高效率, 编译器把代码做出优化, 把一些本来要读内存的操作, 优化成读其寄存器, 减少读内存的次数, 也就可以提高整体程序的效率了

举个例子:

public class Test {private static int isQuit = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(isQuit == 0) {}System.out.println("t1 退出");});t1.start();Thread t2 = new Thread(() -> {System.out.println("请输入 isQuit: ");Scanner sc = new Scanner(System.in);isQuit = sc.nextInt();});t2.start();}
}

当我们输入 isQuti == 1 或者其他不为零的数时程序应该停止运行, 但结果不是. 打开 jconsole 可以看到 t1 线程正在执行, 为 RUNNABLE 状态

之前是两个线程修改用一个变量会引起线程安全问题, 现在是一个线程读, 一个线程修改也有可能会有问题, 就是因为 内存可见性引起的

 

使用方法:

在 isQuit 变量加上 volatile限制就可以了

关于内存可见性还涉及到一个关键概念, JMM(Java Memory Model, Java 内存模型) Java规范文档的叫法.

JMM 把存储空间分为 主内存 和工作内存, t1线程对应 isQuit 变量, 本身是在 主内存中的, 由于此处的优化就会把 isQuit 变量放到工作内存中. 进一步的 t2 修改主内存的 isQuit, 不会影响到 t1 的工作内存. 主内存就是咱们平常说的内存, 工作内存就是 CPU 寄存器.

volatile 可以保证内存可见性, 但是不能保证原子性

wait 和 notify (重要)

wait 和 notify 都是 Object 方法, 随便定义一个对象, 都可以使用 wait notify .它俩需要配合使用

作用: 用来协调多个线程的执行顺序

本身多个线程的执行顺序是随机的 (系统随即调度, 抢占式执行) 很多时候希望通过一定手段, 协调执行顺序. join 使用像线程结束的先后顺序, 相比之下此处是希望线程不结束, 也能够有先后顺序的控制.

wait 等待:  让指定线程进入阻塞状态

notify 通知:  唤醒对应的阻塞状态的线程

wait如何使用

public class Test {public static void main(String[] args) throws InterruptedException {Object object = new Object();System.out.println("wait 之前");object.wait();System.out.println("wait 之后");}
}

此处代码报错, 非法的 监视器 状态 异常 , 其中 synchronized 就是监视器锁. 

wait 操作在执行的时候要做 三件事

1> 释放当前的锁

2> 让线程进入阻塞

3> 线程被唤醒的时候重新获取到锁

通过object 调用wait 释放锁的过程. 释放锁的前提就是 先加锁

public class Test {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("wait 之前");//将 wait 放到 synchronized 里面调用, 保证确实是拿到了锁object.wait();System.out.println("wait 之后");}}
}

应该把 wait 写到 synchronized 里面加锁, 此时可以运行代码, 但是wait 会持续阻塞等待下去, 直到其他线程调用 notify 唤醒. 

此处的状态就是 waiting 状态

wait 除了默认的无参版本之外, 还有一个带参数的版本, 带参数的版本就是指定一个时间

参数的版本就是指定超时时间, 避免 wait 无休止地等待下去.

notify如何使用

public class Test {public static void main(String[] args){Object object = new Object();Thread t1 = new Thread(() -> {synchronized (object) {System.out.println("wait 之前");try {object.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("wait 之后");}});Thread t2 = new Thread(() -> {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object) {System.out.println("进行通知");object.notify();}});t1.start();t2.start();}
}

 

结果如图.

" 线程饿死 "

针对这种情况:可以使用 wait 和 notify 来解决

让 1 号老铁在发现没钱的时候就进行 wait.(wait 内部本身就会释放锁, 并且进入阻塞)

让 1 号老铁不再进行后续的锁竞争, 把所释放出来让别人获取. 给其他老铁提供机会

运钞车把钱运过来的线程就是调用 notify 唤醒的线程

notify 和 notifyAll

notify 一次唤醒一个线程

notifyAll 一次唤醒全部线程

调用 wait 不一定只有一个线程调用, N 个线程都可以调用 wait, 此时有多个线程调用的时候这些线程都会进入阻塞状态., 唤醒的时候就有两种方式了

nitifyAll: 唤醒的时候 wait 涉及到一个重新获取锁的过程, 需要串行执行(用的更少)

notify: 更可控(用的更多)

小结

保证线程安全需要 : 保证原子性, 可见性, 顺序性:

了解synchronized 和 wait notify , volatile 的语法, 目的

掌握死锁的几种情况, 及如何解决死锁问题.

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

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

相关文章

SpringBoot中使用MongoDB

目录 搭建实体类 基本的增删改查操作 分页查询 使用MongoTemplate实现复杂的功能 引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> 在ap…

CentOS 8.5 安装配置 Tinyproxy 轻量代理服务器 Windows10 系统设置http代理 详细教程

1 下载 下载地址 2 上传服务器并解压 tar zxvf tinyproxy-1.11.2.tar.gz 3 安装配置 #安装依赖软件 yum install automake cd tinyproxy-1.11.2/ #生成configure ./autogen.sh # ./configure --prefix/usr/local/tinyproxy make make install 4 配置环境 vim /etc/prof…

【教程】最新MySQL8.3.0社区版安装指南(超详细)

写在前面&#xff1a; 如果文章对你有帮助&#xff0c;记得点赞关注加收藏一波&#xff0c;利于以后需要的时候复习&#xff0c;多谢支持&#xff01; 文章目录 一、下载安装包二、解压安装包三、设置配置文件四、配置系统环境五、初始化操作 此次安装的版本为MySQL社区版&…

【JVM】Class文件的格式

目录 概述 Class文件的格式 概述 Class文件是JVM的输入&#xff0c;Java虚拟机规范中定义了Class文件的结构。Class文件是JVM实现平台无关、技术无关的基础。 1:Class文件是一组以8字节为单位的字节流&#xff0c;各个数据项目按顺序紧凑排列 2:对于占用空间大于8字节的数据…

实验室信息管理系统主要解决哪些问题,能帮实验室从哪些方面提升效率?

实验室信息管理系统&#xff08;LIMS&#xff09;是一种全面精益化管理工具&#xff0c;它对实验室的人、机、料、法、环进行精确管理&#xff0c;使监测业务高效、准确、方便&#xff0c;确保实验室的运行效率和数据安全性得到极大的提升。通过LIMS&#xff0c;实验室能够实现…

Android Studio连接MySQL8.0

【序言】 移动平台这个课程要做一个app的课设&#xff0c;我打算后期增加功能改成毕设&#xff0c;就想要使用MySQL来作为数据库&#xff0c;相对于SQLlite来说&#xff0c;我更熟悉MySQL一点。 【遇到的问题】 一直无法连接上数据库&#xff0c;开始的时候查了很多资料&#…

qt for android 的架构原理

qt for android实现架构&#xff0c;分享这几幅很不错图。来自于 《Qt 林斌&#xff1a;整合Android IVI平台打造统一的Qt数字座舱体验》 1.实现架构图 2.qt for android能力 3.java 和 qt混合开发 4. AutoMotive

微信小程序按钮去除边框线

通常我们去掉按钮边框直接设置 border:0 但是在小程序中无效&#xff0c;设置outline:none也没用&#xff0c;当然可能你会说加权重&#xff1b;试过了无效 实际上该样式是在伪元素::after内&#xff0c;主要你检查css 还看不到有这个关系&#xff0c;鹅厂就是坑多 类样式::…

从loss角度理解LLM涌现能力

如今的很多研究都表明小模型也能出现涌现能力&#xff0c;本文的作者团队通过大量实验发现模型的涌现能力与模型大小、训练计算量无关&#xff0c;只与预训练loss相关。 作者团队惊奇地发现&#xff0c;不管任何下游任务&#xff0c;不管模型大小&#xff0c;模型出现涌现能力…

Axure RP移动端交互元件库/交互原型模板

作品类型&#xff1a;元件库/原型模板 更新日期&#xff1a;2023-12-04 当前版本&#xff1a;V1.3 适用范围&#xff1a;App应用/小程序 Axure版本&#xff1a;Axure 9.0均可打开 文件大小&#xff1a;36.7M 历时两个月制作并整理了手机移动端常用的75种组件、90个常用界面模板…

【算法】最短路问题 bfs 到 dijkstra

1976、到达目的地的方案数 你在一个城市里&#xff0c;城市由 n 个路口组成&#xff0c;路口编号为 0 到 n - 1 &#xff0c;某些路口之间有 双向 道路。输入保证你可以从任意路口出发到达其他任意路口&#xff0c;且任意两个路口之间最多有一条路。 给你一个整数 n 和二维整…

使用socat做端口转发

最近买的云上mongo数据库但是数据库不支持外网访问&#xff0c;准备做iptables转发但是一直不成功&#xff0c;腾讯云官方给予的解释是受服务器内启动的docker影响 做iptables转发会冲突&#xff0c;所以只能另想办法&#xff0c;我发现使用socat做转发也很好用&#xff0c;所以…

C语言基础——循环语句

&#x1f33a;​&#x1f64f;&#x1f64f;&#x1f64f;欢迎大家观看&#xff0c;写的好的话希望三连感谢&#x1f64f;&#x1f64f;&#x1f64f;&#x1f33a; 文章目录 一、循环语句的介绍 二、不同循环语句的使用 1.while循环 1.1 while循环的使用方式 1.2 while循环的执…

初识sql注入--手工注入

目录 可能使用的sql函数 入侵网站方式 1、文件上传漏洞 2、rce 3、sql注入 SQL注入 什么是sql注入 进行SQL注入 实验环境 开始实验&#xff08;使用information_shema数据库&#xff09; 1、进入靶场 2、报列数 下面来解释一下为什么要照上面SQL语句写 url编码 单…

六级翻译笔记

理解加表达 除了专有名词不能自己理解翻译&#xff0c;其它都可以 时态一般唯一 题目里出现有翻译为 客观存在&#xff1a; there be 单词结尾加er和ee的区别&#xff1a;er是主动&#xff0c;ee是被动 中文句子没有被动&#xff0c;也可以英文翻译为被动 中文的状语可以不是…

【Linux笔记】 基础指令(二)

风住尘香花已尽 日晚倦梳头 重命名、剪切指令 -- mv 简介&#xff1a; mv 命令是 move 的缩写&#xff0c;可以用来移动文件或者将文件改名&#xff0c;是 Linux 系统下常用的命令&#xff0c;经常用来备份文件或者目录 语法&#xff1a; mv [选项] 源文件或目录 目标文件或目录…

stm32——OLED篇

技术笔记&#xff01; 一、OLED显示屏介绍&#xff08;了解&#xff09; 1. OLED显示屏简介 二、OLED驱动原理&#xff08;熟悉&#xff09; 1. 驱动OLED驱动芯片的步骤 2. SSD1306工作时序 三、OLED驱动芯片简介&#xff08;掌握&#xff09; 1. 常用SSD1306指令 2. …

iOS 提交项目到github(本地没有该项目)

流程简介 申请github账号&#xff08;如果有请跳过&#xff09; add repository创建项目开心的提交就好 具体过程 1. 申请账号&#xff08;本部分不做介绍&#xff0c;请自行研究&#xff09; 2. 如果有账号&#xff0c;按照下面图片依次操作就好 点击该图中的New reposito…

AI语音模型PaddleSpeech踩坑(安装)指南

PaddleSpeech简介 PaddleSpeech 是基于飞桨 PaddlePaddle 的语音方向的开源模型库&#xff0c;用于语音和音频中的各种关键任务的开发&#xff0c;包含大量基于深度学习前沿和有影响力的模型。 PaddleSpeech安装步骤 提示&#xff1a;要找到一个合适的PaddleSpeech版本与pad…

sklearn之线性回归——以上证红利指数为例

文章目录 线性回归概念使用sklearn实现上证中立指数预测内置数据集的加载与处理 外部数据集的加载和处理数据内容数据加载和处理 开始预测分割数据集导入线性回归模型查看线性回归模型的系数绘制预测结果预测效果评估 最终代码 线性回归 线性回归&#xff08;Linear Regressio…