【多线程】线程安全

news2024/11/15 12:58:15

 


目录

1.观察线程不安全

 2.线程不安全的原因

 2.1 随机调度

2.2 修改共享数据

 2.3 原子性

2.4 内存可见性

2.5 指令重排序

3.synchronized 加锁操作

3.1 synchronized是什么?

3.2 synchronized的特性 

1) 互斥 

2) 可重入

 3.3 synchronized使用示例

 3.3.1 针对指定对象加锁

 3.3.2 针对this加锁

3.3.3 针对类对象加锁

 3.3.4 疑难杂症


 多线程编程可以提高系统的并发性响应性,但也会带来一些风险和挑战。这就涉及到了一个非常重要的问题-线程安全(重点)

1.观察线程不安全

这是一段线程不安全的代码

public class Main {
    private static int count = 0;//此处定义一个Int类型的变量
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           //对count变量进行自增5w次
            for (int i = 0; i < 50000; i++) {
                count++;
            } 
        });
        Thread t2 = new Thread(()->{
           //对count进行自增5w次
            for (int i = 0; i < 50000; i++) {
                count++;
            } 
        });
        t1.start();
        t2.start();
//        t1.join();
//        t2.join();

        System.out.println("count: " + count);
    }
}

按照我们的逻辑来说,两个线程分别让count自增了5w次,那么最后打印出来的结果应该是10w次才对,此处如果没有 join() 那么最后的结果会是10w次吗?我们来看代码的结果:

为什么答案是0?

当两个线程start的时候,此时start方法就分别去创建一个线程,然后这个线程去调用run方法(这里不赘述,详细请看博主之前的博客Thread类)这个过程需要一定时间,

但是由于主线程自上而下在运行,start方法后,就已经执行到对count的打印了,还没等到两个线程去完成count的自增,这个进程就结束了。

那么我们接下来让主线程等待两个线程完成了对count的自增,最后再打印count值,这个值会是10w吗?

第一次执行结果:

第二次执行结果:

第三次执行结果:

我们可以清晰看到三次执行的结果都不是我们预计中的10w次,明明我们预期结果是 10w,但是达不到预期,此时就可以认为程序出现了 BUG!

凡是实际结果与预期结果不同,都认为是出现了 BUG!

上述代码就是典型的线程安全问题,也可以称为线程不安全

为什么会出现这样的问题?是多线程在捣鬼吗?如果我们不使用多线程会出现这样的问题吗?我们来看下面这简单的代码:

public class Main {
    private static int count = 0;
    public static void main(String[] args) {
            for (int i = 0; i < 100000; i++) {
                count++;
            }
            System.out.println("count: " + count);
    }
}

 结果如下:

结果跟我们预计的一样,如此便可以得到结论:出现预计结果不一致的问题出在多线程身上!

那么为什么使用多线程会造成这样的问题呢?

答案便是:代码执行顺序的不确定性

我们要知道多线程是同时执行多个线程的机制,每个线程独立执行,拥有自己的执行路径和执行状态。不同线程执行的速度和顺序是无法确定的,而且在多核处理器上,这些线程可能会同时并行执行。

多个线程同时访问共享资源时,比如同一个变量或同一个文件,可能会出现竞争条件(Race Condition)。竞争条件可能会导致程序出现错误的结果或异常情况。

这样我们便可以进一步得出结论:

  • 如果没有多线程,代码的执行顺序是固定的,代码执行顺序固定,程序的结果也就是固定的!

  • 如果使用多线程,代码的执行顺序会出现更多的变数,执行顺序的可能性由于 CPU 的随机调度,可能出现了无数情况!

所以我们在使用多线程的时候,要解决执行顺序不确定的问题,确保每次执行的结果都正确!

而要解决这个问题,我们首先要理解 count++ 这个操作都做了什么?

count++这个自增操作看起来很不起眼,实际上在操作系统中,++这个操作是比较麻烦的事情,这个操作要分成三个步骤来完成:

  1. 先把内存中的值,读取到 CPU 寄存器中 (load)

  2. 把 CPU 寄存器的数值进行 +1 运算 (add)

  3. 把得到的结果写回到内存中 (save)

 由于 CPU 是随机调度的,所以就可能出现以下的情况:

第一种情况:t1线程完成count的自增但是还没有把结果返回,此时t2线程就读取了count的值

这种情况造成两次提交的结果,count只加了一次!

像不像数据库中的“读脏数据”?(一个事务更新的数据尚未提交,被另一事务读到)

第二种情况:t1线程完成了自增并返回结果,t2线程再开始读数据,然后自增返回结果

这种情况就是正常的两次提交,count加了两次! 是跟我们的预期结果一致的情况!

所以这就是为什么上述代码每次打印的结果都不同了,那有没有可能刚好打印 10w 呢?也是有可能的!但是这个概率真的很低哈!!

 我们已知造成上述代码结果与我们预期不一致的原因是因为线程调度是随机的!那么,线程不安全的原因还有哪些呢?我们接着往下看

 2.线程不安全的原因

 2.1 随机调度

线程调度是随机的,这是线程安全问题的罪魁祸首!

随机调度使一个程序在多线程环境下,执行顺序存在多种情况

程序猿必须保证 在任意执行顺序下,代码都能够正常的工作! 

 这个问题在上述 count++ 的例子中,已经体现了,这里就不再赘述

2.2 修改共享数据

也就是多个线程修改同一个变量

在我们上述的线程不安全代码中,我们使用了2个线程针对count变量进行修改

此时这个count是一个多个线程都能访问到的“共享数据” 

 为什么修改共享数据会使线程不安全呢?其实我们上面的线程不安全代码也体现了:

  1. 多个线程对共享数据进行修改时,它们的执行顺序是不确定的。(本质)
  2. 当多个线程同时对共享数据进行修改时,可能会导致数据的不一致性。
  3. 由于每个线程都有自己的缓存,可能会导致缓存中的数据与主存中的数据不一致。

 2.3 原子性

什么是原子性呢?原子性是指一个操作是不可分割的,要么完整地执行,要么不执行。其实这个概念的提出是因为当时人们误以为原子是物质最小的单位,认为原子不可再拆分,不像我们如今还发现了中子、质子等更小的单位! 

我们要注意,一条java语句不一定是原子的,也不一定只有一条指令! 

比如我们上述代码中的 count++ ,其实是由三步操作组成的!

  1. 从内存把数据读取到CPU
  2. 进行数据更新
  3. 把数据写回到CPU

不保证原子性,可能会出现我们上述代码例子中,一个线程中的add、save、load三步操作与别的线程的三步操作乱序进行的结果!

2.4 内存可见性

一个线程对共享变量值的修改,能够及时地被其他线程看到,那么便不会出现问题,如果没有被及时发现,那么其他线程可能读到了一个修改之前的值,也会造成线程不安全

这也是前文所说的数据库中的“读脏数据”的感觉!

2.5 指令重排序

 指令重排序其实就是编译器好心办坏事!为什么这么说呢?我们看下面的例子:

木兰辞都学过吧?其中有这么一小段:东市买骏马,西市买鞍鞯,南市买辔头,北市买长鞭

于是木兰就去买东西了!她这样买东西:

这样是不是很麻烦?东跑一下,西跑一下,木兰都不知道自己哪里去了,哪里没去,而且多走了没必要的路程,于是优化了一下购买顺序:

 按照这样的顺序,木兰买东西的效率就高多了

指令重排序,就像上述一样,可以少走一些路,优化了效率。 

编译器对于指令重排序的前提是 "保持逻辑不发生变化",对于单线程环境来讲,比较容易判断,但是在多线程的环境下就没那么容易了,多线程代码执行复杂程序更高,编译器很难在编译阶段对代码的执行结果进行预判,因此编译器激进的指令重排序很容易导致重排后的逻辑和之前不等价,就比如:木兰家要为木兰送行,临别吃顿丰盛的饭,要买好几样木兰喜欢吃的菜,于是木兰的弟弟就跟木兰去买东西,要买的东西多了,两个人就容易买重复或者少买了一些东西

3.synchronized 加锁操作

3.1 synchronized是什么?

synchronized是Java语言中的一个关键字,用于实现线程的同步。当一个方法或一个代码块被synchronized修饰时,只有一个线程可以进入该方法或代码块,其他线程需要等待,直到获得锁才能执行。这样可以保证多个线程在访问共享资源时的安全性,避免出现数据不一致或冲突的问题。

3.2 synchronized的特性 

1) 互斥 

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

  • 进⼊ synchronized修饰的代码块,相当于加锁
  • 退出 synchronized修饰的代码块,相当于解锁

比如说:学校的校花受很多人欢迎,结果有一天成了我的女朋友,那么在我没有和她分手之前,其他男生都不可以去追她

注意理解阻塞等待

针对每⼀把锁,操作系统内部都维护了⼀个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程,再来获取到这个锁.

注意:

  • 上⼀个线程解锁之后,下⼀个线程并不是立即就能获取到锁.而是要靠操作系统来"唤醒".这也是操作系统线程调度的⼀部分⼯作.(谁家刚分手就立马和人好上了,那是给绿了吧?)
  • 假设有ABC三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C
    都在阻塞队列中排队等待.但是当A释放锁之后,虽然B⽐C先来的,但是B不⼀定就能获取到锁,而是和C重新竞争,并不遵守先来后到的规则(有种东西叫做一见钟情,不是你追的久你就更有机会)

synchronized的底层是使用操作系统的mutex lock实现的 

2) 可重入

synchronized同步块对同⼀条线程来说是可重⼊的,不会出现自己把自己锁死的问题; 

什么是"把自己锁死"?

⼀个线程没有释放锁,或者又尝试再次加锁
 //第⼀次加锁,加锁成功
lock();
//第⼆次加锁,锁已经被占用,阻塞等待.
lock();

按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待.直到第⼀次的锁被释放,才能获取到第⼆个锁.但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作.这时候就会死锁

 

 这样的锁称为不可重⼊锁.

Java中的synchronized是可重⼊锁,因此没有上面的问题.

for (int i = 0; i < 50000; i++) {
    synchronized (locker) {
        synchronized (locker) {
            count++;
        }
    }
}

 在可重⼊锁的内部,包含了"线程持有者"和"计数器"两个信息.

  • 如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增. 
  • 解锁的时候计数器递减为0的时候,也就是才真正释放锁.(才能被别的线程获取到)

例如网红疯狂小杨哥之前的作品中,就有一个道具,超级无敌居多锁的铁箱!为了不让老弟玩电脑,老哥把主机锁进去了,然后加上了N把锁 ,这个时候老弟想要玩电脑,只能解完这N把锁,减为0的时候,才能拿到主机!感兴趣的可以看看该作品

 

 3.3 synchronized使用示例

 3.3.1 针对指定对象加锁

目录2中提到了造成线程不安全的5个原因,其中“随机调度”,线程抢占式执行,这个是操作系统内核规定的,我们无法更改,我们收拾不了随机调度,还收拾不了“修改共享数据”吗?我们可以不允许线程访问同一变量,但是这样就削弱了多线程优势了!既然“随机调度”收拾不了,“修改共享数据”又不好下手,那我们接着只能从“原子性”下手了,柿子还是得找软的捏!

通过原子性,我们可以解决指令不是原子性所造成的线程安全问题!

回顾目录3.1,当一个方法或一个代码块被synchronized修饰时,只有一个线程可以进入该方法或代码块,其他线程需要等待,直到获得锁才能执行。

也就是说:synchronized 关键字进行加锁,可以保证加锁的代码块是原子性的!

public class ThreadDemo {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        // 下面的等待是为了让两个线程都自增完成
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}
// 第一次执行打印结果:count = 100000
// 第二次执行打印结果:count = 100000
// 第三次执行打印结果:count = 100000

上述代码针对 object 这个对象加锁,一个对象只有一把锁

当 t1 线程执行到 count++ 时就会尝试获取 object 的锁,如果获取到了,就进行加锁操作(尝试打开心上人的心房,进去后就上锁!) 并执行 count++,只有执行完 synchronized 代码块的内容后,才会自动释放锁(感情走到了终点,你会自动开锁,走出他的心房) 

如果 t1 在执行 count++ 的过程中,t2也执行到 count++ 了,此时 t2 就会尝试获取 object 对象锁,但是 object 已经被 t1 加锁了,那么 t2 就只能阻塞等待(狭路相逢勇者胜!已有对象请等待!)等 t1 释放锁,t2 才能获取锁,并加锁!那么 t1 只有执行完 synchronized 代码块后,自动释放锁!

synchronized 用的锁是JAVA对象头里的,可以粗略理解成每个对象在存储的时候,都有一块内存表示当前 "锁定" 状态

 3.3.2 针对this加锁

 在3.3.1中是针对object对象进行加锁,而这里可以针对this对象来进行加锁

public class Demo {
    public int count = 0;
    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

this是对当前对象的引用,针对this对象来加锁,也就是针对当前对象进行加锁。

当我们调用increment方法时候,就需要一个Demo类型的对象来调用方法,谁调用了increment方法,谁就是这个this所引用的对象,也就是针对这个所引用的对象进行加锁

public static void main(String[] args) throws InterruptedException {
    Demo d = new Demo();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            demo.increment();
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            demo.increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(d.count);
}
// 打印结果:count = 100000

此时通过 d 这个对象调用了 increment 方法,就是针对 d 这个对象加锁!

上述代码如果 t1 和 t2 线程都执行到了 d.increment() 里的 synchronized 代码块,此时就会发生锁竞争。当 t1 竞争到锁了,此时 t2 就需要阻塞等待,等到 t1 执行完 synchronized 代码块后释放锁了,t2 才能尝试获取锁。

注意:锁竞争,也叫锁冲突,只有在多个线程争同个锁时才会发生!

 同时针对 this 加锁也可以写成如下所示:

public class Demo {
    public int count = 0;
    synchronized public void increment() {
        count++;
    }
}

此时也是针对 this(当前对象) 加锁,只不过这里进入 increment 方法就会加锁,结束 increment 方法就会自动释放锁

3.3.3 针对类对象加锁

类对象是什么?

类对象(Class Object)是指代表一个类的对象。在Java中,每个类都有一个对应的类对象,该类对象存储了类的信息,包括类的名称、属性、方法等。类对象可以通过反射来获取。

当我们针对静态方法加锁时,就是针对类对象加锁:

public class Demo {
    synchronized public static void hello() {
        System.out.println("hello world");
    }
}

 此时也就是相当于针对 Demo.class 这个对象加锁!

 

 3.3.4 疑难杂症

针对 synchronized 关键字 ,我们首先要记住这个单词,不能拼错,这个是最基本的!然后我们还要注意这个关键字是针对哪个对象加锁

上面我们一共讲了3个针对不同对象的加锁:

  1. 针对指定对象加锁;
  2. 针对当前对象加锁;
  3. 针对类对象进行加锁

下面我创建一个demo类来举例:

例1:synchronized 修饰了 func1 和 func2 方法,如果 t1 线程正在执行 func1() 方法,此时就针对  d 这个类的实例化对象加锁了,如果此时 t2 想执行 func2() 就不行了!因为 d 已经被 t1 加锁了!

import static java.lang.Thread.sleep;

class demo{
    synchronized public void func1() throws InterruptedException {
        System.out.println("func1执行中");
        System.out.println("停5秒,看func2是否执行");
        sleep(5000);
        System.out.println("5秒后");
    }

    synchronized public void func2() throws InterruptedException {
        System.out.println("func2执行中");
        sleep(1000);
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        demo d = new demo();
        Thread t1 = new Thread(()->{
            try {
                d.func1();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t2 = new Thread(()->{
            try {
                d.func2();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("执行完毕");
    }
}

上述代码中,demo类有两个被 synchronized 修饰的方法func1和func2,然后在主线程中创建了两个线程t1和t2,分别执行 func1方法 和 func2方法 ,我们看运行结果:

 很明显,t1在执行 func1方法 时候,t2无法执行 func2方法!

上述代码中是否存在BUG了?

前面讲过了,线程安全问题最本质的原因是因为操作系统的随机调度,线程的抢占式执行,那么有没有一种可能,t2线程先t1一步,对 d 加锁了,导致t2先执行?

所以上述的代码是存在线程安全问题的! 

例2:synchronized 修饰了 fun1 方法但是没有修饰 func2 方法,如果 t1 线程正在执行 func1(),此时 t2 仍然能执行 func2(),因为 func2 没有被 synchronized 修饰。

例3:synchronized 修饰了静态的 func1 方法,也修饰了普通的 func2 方法,如果 t1 线程正在执行 demo.func1(),此时 t2 能执行 d.func2 方法,因为是针对不同的对象加锁,t1 是针对 demo.class 类对象加锁,而 t2 是针对 d 实例化对象加锁。(例1和例2的方法都是普通方法)

注意区分 类对象和实例化对象,二者不同! 

类对象类的表示,它存储了类的静态成员(如静态方法、静态变量)和类的元信息。类对象是在类被加载时创建的,并且在整个程序运行期间只存在一份

实例化对象是通过类创建的具体对象,它存储了类的非静态成员(如实例变量)和实例方法的具体值。每次创建类的实例对象时,都会在内存中分配一块新的空间

类对象和实例化对象具有不同的属性和行为。类对象用于管理类的静态成员和提供类级别的操作而实例化对象用于存储类的实例变量和提供实例级别的操作

 

感谢观看,希望对您有所帮助! 

下期预告:死锁问题

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

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

相关文章

安卓手机APP开发__平台的架构

安卓手机APP开发__平台的架构 目录 概述 安卓软件栈 Linux内核 硬件抽象层(HAL) 安卓运行时 原生的C/C代码库 Java API框架 系统APP 概述 安卓是一个开源的&#xff0c;基于Linux的软件栈&#xff0c;它创建一个设备和形式因素的很宽的矩阵。 下图展示了安卓平台的所有…

MyBatis-Plus 从入门到精通

MyBatis-Plus 从入门到精通 前言快速入门创建一个SpringBoot项目导入依赖配置数据库创建一个实体类创建一个mapper接口在SpringBoot启动类上配置mapper接口的扫描路径在数据库中创建表编写一个SpringBoot测试类 核心功能注解CRUD接口Mapper CRUD接口Service CRUD 接口条件构造器…

C字符串和内存函数介绍(二)——长度不固定的字符串函数

前面我们一起学习了strlen&#xff0c;strcpy&#xff0c;strcmp&#xff0c;strcat的使用以及它们的模拟实现&#xff0c;它们的特点是你传参的时候&#xff0c;传过去的是数组首元素的地址&#xff0c;然后无论是计算长度&#xff0c;实现拷贝&#xff0c;相互比较还是进行追…

布局、基本控件

一、as布局 布局文件 layout drawable 设置背景的文件 新建drawable-xhdpi文件 — 放一些item或图片 values&#xff1a; theme app风格&#xff0c;string 字符串&#xff08;相当于宏定义&#xff0c;可以引用&#xff09;&#xff0c;colors颜色配置&#xff08;可以引用…

新闻稿推广策略有哪些?建议收藏

新闻稿推广是一种有效的公关工具&#xff0c;它可以帮助企业或组织传递信息、提升品牌知名度、建立权威形象&#xff0c;并最终促进销售。新闻稿推广策略有哪些&#xff1f;接下来伯乐网络传媒就来给大家分享一下。 确定目标受众&#xff1a;在撰写新闻稿之前&#xff0c;明确你…

MATLAB导入导出Excel的方法|读与写Excel的命令|附例程的github下载链接

前言 前段时间遇到一个需求&#xff1a;导出变量到Excel里面&#xff0c;这里给出一些命令&#xff0c;同时给一个示例供大家参考。 MATLAB读/写Excel的命令 在MATLAB中&#xff0c;可以使用以下命令来读写Excel文件&#xff1a; 读取Excel文件&#xff1a; xlsread(filen…

文件加密软件排行榜前十:好用的文件加密软件推荐

文件加密软件是企业保护数据安全的有效手段。 选择文件加密软件时&#xff0c;不仅要考虑加密强度&#xff0c;还要兼顾易用性、兼容性及是否满足特定需求。 下面小编将推荐几款好用的文件加密软件&#xff0c;比如以下十款软件排行榜。 以下十款软件各有所长&#xff0c;有…

全能集成开发平台Team·IDE

三甲医院的床位太难等了。反正也是小手术&#xff0c;老苏周五在附近找了家二甲医院&#xff0c;幸运的是&#xff0c;门诊迅速为我开具了入院证。周六早晨就接受了手术&#xff0c;周日挂了一天水&#xff0c;周一下午就出院了。准备在家先休息两天。 2~4 周之后把支架取出来…

数据挖掘实战-基于余弦相似度的印度美食推荐系统

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

机器人非线性控制系统:相关知识点整理

机器人控制系统本质上是一个非线性系统。引起机器人非线性因素很多&#xff0c;机器人的结构、传动件、驱动元件等都会引起系统的非线性。 机器人控制系统是由多关节组成的一个多变量控制系统&#xff0c;且各关节间具有耦合作用。具体表现为某一个关节的运动&#xff0c;会对…

全球AI新闻速递527

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步&#x1f91d;&#x1f91d; 一位上进心十足的【Java ToB端大厂…

一文讲清楚SpringBoot项目打包jar后运行报错template might not exist - 第514篇

历史文章&#xff08;文章累计500&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 《…

机器人回调接口完善

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。 免责声明&#xff1a;该工具仅供学习使用&#xff0c;禁止使用该工具从事违法活动&#xff0c;否则永久拉黑封禁账号&#xff01;&#xff01;&#xff01;本人不对任何工具的使用负责&am…

京东面试:SpringBoot同时可以处理多少请求?

面试题大全&#xff1a;www.javacn.site Spring Boot 作为 Java 开发中必备的框架&#xff0c;它为开发者提供了高效且易用的开发工具&#xff0c;所以和它相关的面试题自然也很重要&#xff0c;咱们今天就来看这道经典的面试题&#xff1a;Spring Boot 同时可以处理多少个请求…

拥塞控制的自适应 AQM 探索

拥塞控制面临的几类问题&#xff1a; 网络拥塞时&#xff0c;大象流如何为微突发让路&#xff1b;网络拥塞时&#xff0c;如何只惩罚造成拥塞的流量&#xff1b;网络拥塞时&#xff0c;如何确保小流量不受影响。 既然不想在 host 将流按照大小分类&#xff0c;嫌没意义&#…

内存泄漏案例分享4-异步任务流内存泄漏

案例4——异步任务内存泄漏 异步任务&#xff0c;代指起子线程异步完成一些数据操作、网络接口请求等&#xff0c;通常会使用以下API&#xff1a; Runnbale&#xff0c;Thread,线程池RxJavaHandlerThread 而这些异步任务很有可能操作内存泄漏&#xff0c;下面我们以Rxjava为…

SaaS增长三大策略:从用户获取到留存转化的全链路解析

在SaaS&#xff08;软件即服务&#xff09;行业中&#xff0c;增长是企业成功的关键。然而&#xff0c;要实现持续增长并非易事&#xff0c;需要一套从用户获取到留存转化的全链路策略。 首先&#xff0c;用户获取是SaaS增长的第一步。 这要求企业明确目标用户群体&#xff0…

什么是HTTP代理?适用于哪些场景?

HTTP代理是一种网络代理服务器&#xff0c;它主要用于处理HTTP协议的请求和响应。HTTP代理充当客户端&#xff08;如浏览器&#xff09;和目标服务器之间的中介&#xff0c;允许客户端通过代理服务器来发送HTTP请求&#xff0c;并接收来自服务器的响应。HTTP代理可以分为正向代…

01-内网基础知识

内网基础知识 一、什么是内网&#xff1f; 内网也指局域网&#xff08;Local Area Network&#xff0c;LAN&#xff09;是指在某一区域内由多台计算机互联成的计算机组。一般是方圆几千米以内。局域网可以实现文件管理、应用软件共享、打印机共享、工作组内的历程安排、电子邮…

牛顿迭代法与二分法求根

牛顿迭代法 求平方根 数学迭代式&#xff1a;x[n1] (x[n] a/x[n])/2. 初始化&#xff1a;x[0] a/2. 保持&#xff1a;x[n1] (x[n] a/x[n])/2. 终止条件&#xff1a;|x[n1]-x[n]| < 0.00001时&#xff0c;迭代终止。 代码&#xff1a; #include "stdio.h&qu…