多线程初阶(二)- 线程安全问题

目录

1.观察count++

 原因总结

 2.解决方案-synchronized关键字

(1)synchronized的特性

(2)如何正确使用

语法格式

3.死锁

(1)造成死锁的情况

(2)死锁的四个必要条件

4.Java标准库中的线程安全类

5.volatile关键字

(1)内存可见性问题

原因 

解决方案 

(2)不解决原子性问题

6.wait和notify 

(1)wait()

(2)notify()

(3)线程饿死问题 

7.wait和sleep的对比(面试题)


1.观察count++

我们观察以下代码:

public class Demo20 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

他的逻辑是将count在不同的线程下进行五万次++操作,理想的结果是100000,但由于是并发执行,结果并不能达到预期,每次的结果都不相同,因为多个线程并发执行,引起的bug 
这样的bug称为“"线程安全问题"或者叫做"线程不安全"

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是 线程安全的。

我们从cpu的视角来观察count++操作,它是由3个指令的:

  1.  把内存中的数据读取到cpu寄存器里   load
  2. 把cpu寄存器里的数据+1    add
  3. 把寄存器的值写回内存       save

由于CPU是随即调度,抢占式先行所以在调度线程的时候不知道什么时候会切换线程
指令是cpu执行的最基本单位,要调度,至少把当前执行完,不会执行一半调度走,所以当针对一条指令的时候就不会出现安全性问题;但是由于count++是三个指令,可能会出现cpu 执行了其中的1个指令或者2个指令或者3个指令调度走的情况,这样就会出现线程安全问题产生bug。


无bug的情况:

有bug的情况(出现了覆盖的状况):

 原因总结

  1. 线程在操作系统中是随即调度,抢占式执行的(根本原因)
  2. 多个线程同时修改同一个变量
  3. 修改操作不是“原子”的
  4. 内存可见性问题
  5. 指令重排序问题


原子性:原子是不可分割的最小单位,cpu视角不可分割的最小单位就是一条指令,cpu在进行调度切换线程的时候势必会确保执行完一条指令才能调度走再执行下一条命令,所以像count++, +=,-=之类的操作都不具备原子性  赋值操作a=b是具备原子性的

 2.解决方案-synchronized关键字

针对原因一我们无法干预,操作系统内核,负责的工作,咱们作为应用层的程序员,无法干预

针对原因二取决于实际的需求.有的场景能这么改,有的场景不能这么改取决于实际的需求
在Java中这个方案不算很普适的方案.

针对原因三我们重点进行探讨,该操作不是原子的那怎么可以变成原子的呢
 

进行加锁操作,想象一个上厕所的场景,你对门进行了加锁,这样别人就不能进来,只有当你上完厕所出来才算解锁

注意:此处的加锁操作并非是将count++操作变成原子的,也没有干预到线程的调度,只是通过这种加锁的方式来保证一个线程在执行count++操作的过程中其他线程的count++不能插队进来

(1)synchronized的特性

  1. 互斥:synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行 到同一个对象synchronized就会阻塞等待.

    进入synchronized修饰的代码块,相当于加锁
    退出synchronized修饰的代码块,相当于解锁
    synchronized用的锁是存在Java对象头里的。

  2. 可重入:针对一个线程一把锁.这个线程针对这把锁,连续加锁两次这种情况理论上应该死锁,但由于该特性不会造成死锁

    在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息.
        如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增.
        解锁的时候计数器递减为0的时候,才真正释放锁.(才能被别的线程获取到)
     

(2)如何正确使用

synchronized() {}


synchronized不是函数而是关键字,括号内也不是参数,而是用来指定一个锁对象(可以指定任何对象),通过锁对象来进行后续的判定

{}里面的代码,就是要打包到一起的代码~~
{}还可以放任意的其他代码,包括调用别的方法等合法的java代码
进入代码块就会进行加锁,出代码块就会进行解锁

public class Demo21 {private static int count = 0;private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}

代码解释:t1,t2针对同一个对象locker进行加锁,t1先进行加锁,执行代码块中的代码,此时t2进行等待,t1执行完毕后,t2进行加锁再执行该线程下的代码
(这两者的++操作,不会穿插执行了,也就不会相互覆盖掉对方的结果了)
本质上是把随机的并发执行过程,强制变成了串行,从而解决了刚才的线程安全问题
上述操作能够正确执行的原因是,两个线程都加锁了,并且针对的是同一个对象加锁了

以下两种情况就不能正确执行

  1. 只有一个线程加锁
    public class Demo21 {private static int count = 0;private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker1){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker2){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}

  2. 多线程针对不同的对象加锁
     
    private static int count = 0;private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker1){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker2){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);

以上情况为两个线程针对同一个对象加锁,当第一个线程解锁之后就会执行第二个线程进行加锁;如果是三个线程针对同一个对象加锁,当某个线程先加上锁,另外两个线程开始阻塞等待,此时这两个线程谁先拿到锁是无法预期的,但不存在线程安全问题

多个线程针对同一个对象加锁(大于2)

public class Demo21 {private static int count = 0;private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker1){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker2){count++;}}});Thread t3 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker2){count++;}}});t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();System.out.println(count);}

锁对象的作用:用来区分多个线程是否针对“同一个对象”加锁,
是同一个就会发生“阻塞”(锁竞争/锁冲突)
不是同一个对象就不会发生阻塞,两个线程仍然是随即调度的并发执行

注意事项: 
synchronized关键字本质上比join的串行执行,效率还是要高的
join的串行化是针对线程与线程之间,而synchronized关键字是针对线程中的一小部分逻辑进行加锁来实现串行化

语法格式

修饰类对象
在编写Java代码,本身是.java文件,通过javac编译成.class文件,jvm运行的时候把.class文件加载到内存中进而形成对应的类对象

一个 java进程中一个类的类对象只有唯一一个

private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (Demo21.class){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (Demo21.class){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}


修饰普通方法

class Counter {public int count = 0;public synchronized void add(){count++;}
}class Counter {public int count = 0;public void add(){synchronized (this) {count++;}}
}public class Demo23 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter1();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Counter.count);}
}

修饰静态方法

class Counter {public static int count = 0;public synchronized static void add(){count++;}}public class Demo22 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (Counter.class){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (Counter.class){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

3.死锁

(1)造成死锁的情况

  1. 一个线程一把锁.这个线程针对这把锁,连续加锁两次

     这个情况在代码实例中,并没有真的出现死锁,synchronized针对这个情况做了特殊处理synchronized是“可重入锁”
    针对上述一个线程连续加锁两次的情况做了特殊处理,只有第一次加锁生效,之后的加锁不会生效直接放行

    class Counter1{public static int count = 0;public void add(){synchronized (this) {synchronized (this) {count++;}}}
    }

    那可重入锁是如何判断是否用加锁的情况呢?

  2. 两个线程两把锁
    t1获取锁A,t2获取锁B
    t1获取锁B,t2获取锁A
    public class Demo24 {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){System.out.println("t1加锁成功locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1加锁成功locker2");}}});Thread t2 = new Thread(() ->{synchronized (locker2){System.out.println("t2加锁成功locker2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1){System.out.println("t2加锁成功locker1");}}});t1.start();t2.start();}
    }

  3. N个线程M把锁
    经典哲学家问题:有5个哲学家坐在一起吃饭,但只有5根筷子,哲学家就只做两件事情,一件事情为思考,另一件事情就是吃饭,当其中一个哲学家要吃饭时,就会拿起左右两边的筷子,那么此时如果左右相邻的哲学家也想吃饭时,就需要等待正在吃饭的哲学家吃完饭,放下筷子,才能继续吃,在通常情况下,整个系统可以很好的运转,但是当5个哲学家同时拿起左边的筷子时,就会出现死锁问题

(2)死锁的四个必要条件

  1. 锁是互斥的(锁的基本特性)
  2. 锁是不可被抢占的,线程1拿到锁A后,如果线程1不主动释放A,线程2就不能把锁A抢过来(锁的基本特性)

以上两点对于synchronized这样的锁,互斥和不可抢占都是基本特性,我们无法进行干预
 

  1. 请求和保持。线程1拿到锁A后,不释放A的前提下去拿锁B(代码结构)
        避免出现锁的嵌套即可解决
  2. 循环等待/环路等待/循环依赖 多个线程获取锁的过程存在循环等待(代码结构)
        给锁加编号,约定加锁顺序
     

如果在获取多把锁的时候,不要构成循环等待,就可以了~一~
假设代码按照请求和保持的方式,获取到N个锁,如何避免出现循环等待呢??一个简单有效的办法:给锁编号,1,2,3....N
约定所有的线程在加锁的时候,都必须按照一定的顺序来加锁.(比如,必须先针对编号小的锁,加锁,后针对编号大的锁加锁)

public class Demo24 {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){System.out.println("t1加锁成功locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1加锁成功locker2");}}});Thread t2 = new Thread(() ->{synchronized (locker1){System.out.println("t2加锁成功locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t2加锁成功locker2");}}});t1.start();t2.start();}
}


4.Java标准库中的线程安全类

Java标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施.

ArrayList;LinkedList;HashMap;TreeMap;HashSet;TreeSet;StringBuilder

但是还有一些是线程安全的.使用了一些锁机制来控制.
Vector (不推荐使用)
HashTable(不推荐使用)
ConcurrentHashMap


StringBuffer

5.volatile关键字

(1)内存可见性问题

观察以下代码

public class Demo25 {private static int n = 0;public static void main(String[] args) throws InterruptedException {Thread t1= new Thread(() ->{while (n == 0){//}});Thread t2= new Thread(() ->{Scanner scanner = new Scanner(System.in);System.out.println("请输入一个整数:");n = scanner.nextInt();});t1.start();Thread.sleep(2000);t2.start();}
}


 

原因 


如何进行优化导致出现内存可见性问题的?



此时JVM执行这个代码时发现每次循环过程中(1)操作的开销非常大,而且每次执行(1)操作它的结果都是一样的,并且JVM根本没意识到用户可能未来会修改n,于是JVM就做了一个大胆的操作直接将(1)操作给优化掉了,每次循环不会去读取内存中的数据,而是直接读取寄存器/cache中的数据(缓存的结果)

当JVM做出上述决定后此时意味着,循环的开销大幅度的降低,但是当用户修改n的时候返现内存中的n已经改变了,但是t1线程每次循环不会真的读内存,并没有感知到n的改变,也就是说对于线程t1来说n的改变是“不可见的”,这样就引起了内存可见性的问题


内存可见性问题本质上是编译器/JVM对代码进行优化出现的bug,如果代码是单线程,优化后的代码非常准确,但在多线程中可能会出现误判,这就导致了内存可见性的问题
 

解决方案 

解决方案一:

在t1线程中添加sleep等待
和读内存相比,sleep相比之下就更慢了,足以等到你scanner输入之后t2线程修改后t1感知
 

public class Demo25 {private static int n = 0;public static void main(String[] args) throws InterruptedException {Thread t1= new Thread(() ->{while (n == 0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1线程结束");});Thread t2= new Thread(() ->{Scanner scanner = new Scanner(System.in);System.out.println("请输入一个整数:");n = scanner.nextInt();});t1.start();t2.start();}
}

解决方案二:
添加volatile关键字,该关键字用来修饰一个变量,用来提示编译器这个变量是“易变”的,优化的前提是变量是频繁读取的,而且结果是固定的,此时编译器就会禁止上述优化,以此来确保灭磁都执行从内存中从新读取数据

引入该变量后,编译器生成该代码时,就会给这个变量的读取操作附近生成一些特殊指令,称为“内存屏障”,后续JVM执行到此处时就不会进行优化

public class Demo25 {private static volatile int n = 0;public static void main(String[] args) throws InterruptedException {Thread t1= new Thread(() ->{while (n == 0){//}System.out.println("t1线程结束");});Thread t2= new Thread(() ->{Scanner scanner = new Scanner(System.in);System.out.println("请输入一个整数:");n = scanner.nextInt();});t1.start();t2.start();}
}

(2)不解决原子性问题

public class Demo26 {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

 

6.wait和notify 

线程在操作系统上的调度是随机的,多个线程需要控制线程直接某个逻辑的先后顺序,此时就可以让后执行的逻辑使用wait,先执行的线程,完成某些逻辑之后,通过notify唤醒对方的wait

(1)wait()

作用:

使当前执行代码的线程进行等待(将线程放到等待队列中)

释放当前锁

满足一定条件时被唤醒,重新尝试获取这个锁

结束等待的条件: 
其他线程调用该对象的notify方法.

wait等待时间超时(wait方法提供⼀个带有timeout参数的版本,来指定等待时间).

其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException 异常.

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

非法的监视器状态异常,这里的意思是在调用wait方法时,当前锁的状态是不正确的;很明显此处我们都没加锁又何谈解锁呢?wait方法会针对对象先进行解锁所以要使用synchronized关键字来上锁

加上锁之后由于没有notify解锁,所以会一直等待

wait在执行时会将进行解锁,阻塞等待(目的是为了收到通知)同时执行,这两个操作方法内部已经做好了

(2)notify()

notify方法是唤醒等待的线程.

  • notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait状态的线程。(并没有"先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
     
public class Demo27 {private static Object locker1 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{System.out.println("wait之前");synchronized (locker1) {try {locker1.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("wait之后");});Thread t2 = new Thread(() ->{System.out.println("notify之前");synchronized (locker1) {locker1.notify();}System.out.println("notify之后");});t1.start();t2.start();}

(3)线程饿死问题 

定义︰线程饿死是指一个或多个线程由于某种原因无法获取所需的资源或执行机会,导致它们无法继续正常执行,从而被阻塞在某个状态,不能完成其任务。这种情况通常是由于资源竞争或优先级设置不当导致的。

举例说明,第一个人(t1线程)去取钱并上了锁,但机器里没钱,第一个人可以先出来,可以反反复复进出,这就导致其他人只能干等着,无法获取到锁,此时就会产生线程饿死的情况
 

解决方案:

让第一个人拿到锁的同时进行判定,判定当前能否执行取钱的操作,能则正常执行,不能则主动释放锁,并且进行“阻塞等待”(调用wait实现),此时线程就就不会在后续参与锁的竞争,一直阻塞到取钱的条件具备,此时再由其它线程通知唤醒(notify实现)唤醒这个线程

7.wait和sleep的对比(面试题)

一个是用于线程之间的通信的,一个是让线程阻塞一段时间,

唯—的相同点就是都可以让线程放弃执行一段时间.


1. wait需要搭配synchronized使用. sleep 不需要.
2. wait是Object的方法sleep是Thread的静态方法.

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

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

相关文章

[C/C++入门][for]22、输出奇偶数之和

复习一下我们前面如何判断奇数 判断一个整数是奇数还是偶数&#xff0c;最常用的方法是利用模运算&#xff08;%&#xff09;。模运算符返回除法的余数。对于任何整数n&#xff0c;当你用n % 2&#xff08;n模2&#xff09;来计算时&#xff0c;如果结果是0&#xff0c;那么n就…

异常:android.os.NetworkOnMainThreadException 原因分析

1.错误异常&#xff1a;android.os.NetworkOnMainThreadException 2.原因分析&#xff1a;出现这个错误一般是数据请求在主线程中进行的&#xff0c;所以只要把耗时操作放到子线程中&#xff0c;更新UI在主线中操作。 3.解决方案demo&#xff1a; 创建一个独立的线程&#xf…

若依二次开发

口味改造 原&#xff1a; 改造&#xff1a; 1./** 定义口味名称和口味列表的静态数据 */ 2.改变页面样式 3.定义储存当前选中的口味列表数组&#xff0c;定义改变口味名称时更新当前的口味列表 4.改变页面样式 6.格式转换 7.定义口味列表获取焦点时更新当前选中的口味列表

【DGL系列】简单理解graph.update_all和spmm的区别

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 背景介绍 源码分析 小结一下 背景介绍 我们在看GNN相关的论文时候&#xff0c;都会说到邻接矩阵与特征矩阵之间是用到了spmm&#xff0c;在很久…

深入理解Linux网络(二):UDP接收内核探究

深入理解Linux网络&#xff08;二&#xff09;&#xff1a;UDP接收内核探究 一、UDP 协议处理二、recvfrom 系统调⽤实现 一、UDP 协议处理 udp 协议的处理函数是 udp_rcv。 //file: net/ipv4/udp.c int udp_rcv(struct sk_buff *skb) {return __udp4_lib_rcv(skb, &udp_…

c++ 元组实验

在C中&#xff0c;元组&#xff08;tuple&#xff09;是一种可以存储不同类型元素的数据结构。C11引入了<tuple>库&#xff0c;使得在C中使用元组变得更加容易。下面是一个简单的C元组实验&#xff0c;展示了如何创建元组、访问元组元素以及使用std::get和std::tie等函数…

【Vue3】选项式 API

【Vue3】选项式 API 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的日子。…

thinkphp6连接kingbase数据库

在/vendor/topthink/think-orm/src/db/connector中将Pgsql.php和pgsql.sql文件复制后改名为Kingbase.php和Kingbase.sql Kingbase.php <?php // ---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // …

MySQL中EXPLAIN关键字详解

昨天领导突然问到&#xff0c;MySQL中explain获取到的type字段中index和ref的区别是什么。 这两种状态都是在使用索引后产生的&#xff0c;但具体区别却了解不多&#xff0c;只知道ref相比于index效率更高。 因此&#xff0c;本文较为详细地记录了MySQL性能中返回字段的含义、状…

使用Excel表格还是JSON数据来将数据存入Neo4的选择要素

在选择使用Excel表格还是JSON数据来将数据存入Neo4j时&#xff0c;需要考虑多个因素&#xff0c;包括数据的复杂性、规模、已有的数据处理工具以及你的个人或团队的熟悉度。以下是对两者的一些比较&#xff0c;帮助你做出选择&#xff1a; Excel表格 优点&#xff1a; 直观性…

Oracle(13)什么是外键(Foreign Key)?

外键&#xff08;Foreign Key&#xff09;是一个数据库表中的列或一组列&#xff0c;它们用于建立和强化两个表之间的链接和关系。外键指向另一个表的主键&#xff0c;用于确保数据的一致性和完整性。通过外键&#xff0c;可以保证一个表中的值必须来源于另一个表中的主键值。 …

【web】-反序列化-to_string

<?php highlight_file(__FILE__); class A{public $s;public function __destruct(){echo "hello".$this->s;}} class B{public $cmd;public function __toString(){system($this->cmd);return 1;} } unserialize($_GET[code]); __toString()当对象被当着…

探索特征的隐秘关系:在Scikit-Learn中进行特征交互性分析

探索特征的隐秘关系&#xff1a;在Scikit-Learn中进行特征交互性分析 在机器学习模型中&#xff0c;特征交互性分析是一种揭示特征之间相互作用对模型输出影响的技术。Scikit-Learn&#xff08;简称sklearn&#xff09;&#xff0c;作为Python中广泛使用的机器学习库&#xff…

【Linux服务器Java环境搭建】013 springboot + vue 前后端分离项目详细介绍(理论)

系列文章目录 【Linux服务器Java环境搭建】_一起来学吧的博客-CSDN博客 前言 在之前系列文章Linux服务器Java环境搭建 中&#xff0c;已经在CentOS中将所有环境及所需组件都安装完成了&#xff0c;比如git、jdk、nodejs、maven、mysql、clickhouse、redis、Nginx、rabbitMQ等…

《梦醒蝶飞:释放Excel函数与公式的力量》17.1使用命名范围和工作表函数

第17章&#xff1a;使用命名范围和工作表函数 17.1 命名范围的优势 在Excel中&#xff0c;使用命名范围是一个强大且灵活的功能&#xff0c;它可以极大地提高工作效率和公式的可读性。命名范围不仅使公式更容易理解&#xff0c;还减少了错误的可能性。以下将详细介绍命名范围的…

C++ STL equal_range 用法

一&#xff1a;功能 用于查找元素&#xff0c;它返回了 lower_bound, upper_bound 这两个函数查找结果值。 1. lower_bound 是返回第一个大于等于查找元素的位置。 2. upper_bound 是返回第一个大于查找元素的位置 二&#xff1a;用法 #include <vector> #include &l…

C++案例三:猜数字游戏

文章目录 介绍代码说明设置随机种子生成随机数猜测循环完整代码运行效果介绍 猜数字游戏是一个经典的编程练习,通过这个案例可以学习到基本的输入输出、随机数生成、条件判断和循环结构。 代码说明 设置随机种子 std::srand(static_cast<unsigned int>(std::time(nu…

自然语言大模型介绍

1 简介 最近一直被大语言模型刷屏。本文是周末技术分享会的提纲&#xff0c;总结了一些自然语言模型相关的重要技术&#xff0c;以及各个主流公司的研究方向和进展&#xff0c;和大家共同学习。 2 Transformer 目前的大模型基本都是Transformer及其变种。本部分将介绍Transf…

24暑假算法刷题 | Day18 | LeetCode 530. 二叉搜索树的最小绝对差,501. 二叉搜索树中的众数,236. 二叉树的最近公共祖先

目录 530. 二叉搜索树的最小绝对差题目描述题解 501. 二叉搜索树中的众数题目描述题解 236. 二叉树的最近公共祖先题目描述题解 530. 二叉搜索树的最小绝对差 点此跳转题目链接 题目描述 给你一个二叉搜索树的根节点 root &#xff0c;返回 树中任意两不同节点值之间的最小差…

Python 更换 pip 源详细指南

目录 前言pip 国内源临时换源方法一&#xff1a;添加参数方法二&#xff1a;设置环境变量 永久换源方法三&#xff1a;修改配置方法四&#xff1a;pip 命令修改 总结 前言 在我们使用 Python 3 时&#xff0c;pip 是一个不可或缺的工具&#xff0c;它用于安装和管理第三方库。…