Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字

news2024/9/23 20:26:29

线程安全问题是多线程编程中最典型的一类问题之一。如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的。 通俗来说,线程不安全指的就是某一代码在多线程环境下执行会出现bug,而在单线程环境下执行就不会。线程安全问题本质上是由于线程之间的调度顺序的不确定性,正是这样的不确定性,给我们的代码带来了很多“变数”。

本文将对Java多线程编程中,线程安全问题展开详细的讲解。

目录

一、线程不安全的样例

二、 导致线程安全问题的原因及解决措施

1、***本质原因:线程的无序调度(抢占式执行)

2、多个线程修改同一变量(多线程修改共享数据)

3、修改操作不是原子的

4、内存可见性问题引起的不安全

5、指令重排序引起的不安全

三、解决线程安全问题

1、synchronized关键字:保证修改操作的原子性

(1)什么是加锁

(2)如何进行加锁:synchronized关键字-监视器锁monitor lock

(3)synchronized修饰方法

(4)手动指定一个锁对象

2、volatile 关键字:能保证内存可见性

(1)volatile 修饰的变量,能够保证其 “内存可见性”。

(2)volatile 不保证原子性

3、volatile关键字:禁止指令重排序


一、线程不安全的样例

下面就是一个线程不安全的例子。该代码中创建了一个counter变量,同时分别创建了两个线程t1和t2,让这两个线程针对同一个counter令其自增5w次。

class Counter {
    private int count = 0;

    //让count增加
    public void add() {
        count++;
    }

    //获得count
    public int get() {
        return count;
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线t1和t2,让这两个线程分别对同一个counter自增5w次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        t1.start();
        t2.start();

        // main线程等待两个线程都执行结束,然后再查看结果
        t1.join();
        t2.join();

        System.out.println(counter.get());
    }
}

按理来说,最终输出counter的结果应当是10w次。但我们运行程序后发现,不但结果不是10w,而且每次运行的结果都不一样——实际结果看起来像一个随机值。

那么这里,实际结果与预期结果不相符,就可以认为是出现了由多线程引起的bug,即线程安全问题。


二、 导致线程安全问题的原因及解决措施

1、***本质原因:线程的无序调度(抢占式执行)

线程安全问题的出现与线程的调度随机性密切相关。线程的无序调度也可以理解为抢占式执行。

线程的抢占式执行指的是在多线程系统中,操作系统会对多个线程进行调度,并且可以随时中断正在执行的线程,转而执行另一个线程。

以上述的counter代码为例。count++这一语句,本质上是由3个CPU指令构成:

  1. load。把内存中的数据读取到CPU寄存器中。
  2. add。把寄存器中的值进行 +1 运算。
  3. save。把寄存器中的值写回到内存中。

CPU需要分三步走才能完成这一自增操作。如果是单线程中,这三步没有任何问题;但在多线程编程中,情况就会不同。由于多线程调度顺序是不确定的,实际执行过程中,这俩线程的count++操作的指令排列顺序会有很多种不同的可能:

此处这俩线程的指令的排列顺序(执行先后),有很多种排列的情况

上面只给出了非常小的一部分可能,事实上实际中可能的情况是大量的。而不同的排列顺序下,程序执行的结果可能是截然不同的!我们以其中的两种可能的情况,分析它的执行过程。

注意:t1和t2是两个线程,它们可能运行在不同的CPU核心上,也可能运行在同一个CPU核心上(但是是分时复用也即并发的)。这两种情况的最终效果是一致的,我们选择复杂度相对低的情况来进行演示,即两个线程在不同的CPU核心上运行。

正常的情况

有问题的情况

而所有的指令排列情况中,实际上只有下面这两种情况能得到正确的结果:

“顺序执行”,正确的情况

因此, 由于实际中,线程的调度顺序是无序的,我们并不能确定这俩线程在自增过程中经历了什么,也不能确定到底有多少次指令是“顺序执行”的,有多少次指令是“交错执行”的。最终得到的结果也就成了变化的数值。

针对上述的counter代码,还有一些补充:最终的结果count一定是小于等于10w的,但结果不一定大于5w。参考以下情况:

t1和t2彼此都会给对方带来很多无效自增。如上面图中t2就产生了两次无效自增。不过,count小于5w的情况出现的概率是非常小的。 

归根到底,线程安全问题全是因为线程的无序调度。这导致了线程中指令的执行顺序不确定,从而导致了变化的结果。可以说,线程的无需调度(抢占式执行)是真正的罪魁祸首、万恶之源!

注意:有同学可能会想到,join()能防止线程的抢占执行。不过如果用join(),线程之间是串行执行的,如果这样的话还用多线程干嘛,直接让一个线程串行执行就好了。毕竟,多线程编程的初心就是进行并发编程,更好地利用多核CPU。

2、多个线程修改同一变量(多线程修改共享数据)

这个原因有三个关键词:多个修改同一

也就是说,下面这三种情况,是线程安全的:

  1. 一个线程修改同一个变量(不涉及多线程) -> 安全。
  2. 多个线程读取同一个变量(变量的值不发生变化,最终的结果没有变数) -> 安全。
  3. 多个线程修改不同的变量(各自改各自的,相互之间不影响,和第1条本质上一样) ->安全。

上面的线程不安全的代码中,涉及到了多个线程针对 counter.count 变量的修改。此时,这个 counter.count 是一个多个线程都能访问到的共享数据。

counter.count 这个变量在堆上,可以被多个线程共享访问

3、修改操作不是原子的

原子指的是不可分割的最小单位。

像上面提到的count++自增操作就不是原子的,它可以再拆分成3个操作:load,add,save。单个CPU指令,就不可再拆分。因此,如果某个操作对应单个CPU指令,那么它就是原子的(如赋值操作=);但如果某个操作对应多个CPU指令,它大概率就不是原子的。

假设一个线程正在对一个变量进行操作。中途有其他线程插入进来了,如果这个操作被打断,结果就可能是错误的。 这点也和线程的抢占式调度密切相关。如果线程不是 “抢占” , 就算没有原子性,也问题不大。

正是因为不是原子的,导致两个线程的指令排列存在诸多变数。如果某个操作的原子的,那么指令之间就不会插入其他的指令,指令的排列也就不会存在诸多变数;既然不存在诸多变数,那么结果也就是确定的,此时就是线程安全的。

可以通过synchronized加锁解决这个问题。

4、内存可见性问题引起的不安全

内存可见性引起的不安全问题与上面的counter代码无关,我们重新书写一个演示代码。

先书写这样一段代码:

// 预期:在t2线程中输入一个非0的数,t1线程中循环终止
public class Test2 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 0) {
                // 空着
            }
            System.out.println("循环结束!t1结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner reader = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = reader.nextInt();
        });
        t1.start();
        t2.start();
    }
}

// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

由于两个线程共用同一块内存空间,flag作为一个内存中的变量,两个线程用的是同一个flag。 

对于这段代码,预期的效果是:t1通过 flag == 0 作为条件进行循环,初始情况将进入循环;t2通过控制台输入一个整数,一旦用户输入了非0值,此时 t1 的循环就会立即结束,从而 t1 线程退出。

但当我们运行程序时会发现:当输入非0值1时,t1线程并没有结束。

通过jconsole也能观察到实际效果:t1线程仍然在执行,处于RUNNABLE状态。 

这里,实际效果不等于预期效果,因此再次出现了bug,也即线程不安全问题。 这个问题的出现,就是由内存可见性引起的。

在上面的代码中,while (flag == 0) 中的这个 flag == 0 可以分为两步:load和cmp。其中,load的时间开销远远大于cmp(虽然读内存比读硬盘要快几千倍,但是读寄存器又要比读内存快几千倍)。

并且,该while循环空转的转速极快能达到每秒钟上亿次。(这点非常重要,如果代码中有如sleep()这样大大降低循环执行速度的代码,则不会有上面bug的产生。因为根据下面编译器优化发生的条件,当循环速度(次数)下降,load就已不再是主要开销,编译器就没必要优化了。这样一来,代码是能够正常运行的。)

这时编译器发现:1、load的开销很大;2、每次load的结果都一样。于是编译器就做了一个非常大胆的操作,即把load给优化掉了(就是去掉了)。只有第一次执行load才真正执行了,后续循环都只cmp而不load了(相当于是复用了之前寄存器中的值)。

正常情况:工作内存每次都从主内存中load值
Bug情况:load这一步被取消,工作内存不再每次都从主内存中读取flag值

(这是编译器优化的手段,编译器优化是指能够智能地调整你的代码执行逻辑,在保证程序结果不变的前提下,通过加减语句、语句变换等一些列操作,让整个程序执行的效率大大提升。编译器的优化是一件非常普遍的事情。)

编译器对于“保证程序结果不变”的判定,在单线程下是非常准确的,但是在多线程的情况下就不一定了。可能导致在调整之后,虽然效率高了,但程序结果变了。即:编译器出现了误判,从而引起了bug。

总结:所谓的内存可见性问题,就是多线程环境下编译器对代码优化产生了误判,最终导致我们的代码 出现bug。可以用volatile关键字解决这个问题。

5、指令重排序引起的不安全

指令重排序,也是编译器优化的策略。在保证整体逻辑不变的情况下,编译器通过调整代码执行的顺序,让程序更高效。

比方说你去菜市场买菜,要买:1、土豆,2、黄瓜,3、鸡蛋,4、番茄 这四种菜,这四种菜在菜市场的摊位分布如下:

这时聪明的你肯定想到,既然按照清单上的购买顺序效率太低,那就不按照清单的顺序了,怎么方便怎么顺路,就怎么买~

编译器对程序的优化也是类似的。

但谈到优化,都得保证调整之后的结果和之前是不变的。单线程下容易保证,但多线程就不好说了。比如下面这个代码,就可能会因为指令重排序而出现问题:

这其中,s = new Student(); 这一操作实际上可以分成 3 步: 

 这个过程就好比买房:

先拿到钥匙还是先装修,问题都不大,最终都能拿到房子。

同样的,单线程中,先执行步骤2还是先执行步骤3,最终结果都是相同的。但多线程情况下,就可能出现如下问题:

可以用volatile关键字解决这个问题。


三、解决线程安全问题

1、synchronized关键字:保证修改操作的原子性

(1)什么是加锁

以上述counter代码为例:能有办法让count++操作变成原子的吗?当然是有的,这个方法就是加锁。如果给count++加锁,就能保证count++操作的原子性。

Java中虽然有读写锁,但一般不会特别去区分。默认情况下,用的就是一个很普通的加锁。

加锁的操作就好比我们在学校上厕所。有人进了厕所之后,就要把厕所门锁上,这时候他就可以在里面干一些事情而此时其他人无法进入厕所了。等到他干完了事情,再把锁打开,他就可以从厕所里出来了。当有人正在使用厕所的时候,如果其他人也想用,那么他们只能进行阻塞等待。

锁的核心操作有两个:1、加锁;2、解锁。一旦某个线程加了锁之后,其它线程也想加锁,就不能直接加上,就必须阻塞等待,直到拿到锁的那个线程释放了锁为止。

注意:线程调度是抢占式执行的,当1号老哥释放锁之后,等待锁的2号、3号、4号、5号谁能抢先一步拿到锁、成功加锁,是不确定的。 每个线程都有概率拿到锁,这完全看系统是如何调度的。

(2)如何进行加锁:synchronized关键字-监视器锁monitor lock

synchronized的读音:https://fanyi.baidu.com/#en/zh/synchronized

synchronized是Java中的关键字,可以直接使用这个关键字来实现加锁效果。

前面提到,锁的两个核心操作是加锁和解锁。此处使用代码块的方式来表示:

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

{ }就可以想象成厕所~

synchronized( 锁对象 ) { }

下面这张图也非常形象地表示锁竞争的情况: 

 注意:( )里的锁对象可以写作任意一个Object对象(是类类型即可,内置类型不行)。此处写了this,相当于将Counter counter = new Counter()的这个counter实例作为锁对象。this指向的是当前对象,add作为成员方法,观察代码可知,每次都是counter实例来调用add,this指向的就是当前对象counter。

使用this,哪个实例调用的add()就是对哪个实例加锁。

在这个代码中,线程t1和t2给同一个锁对象(this,即counter)加了锁,就会产生锁竞争。t1拿到锁,t2就得阻塞。此时就可以保证自增操作count++是原子的,不会受多线程抢占式调度的影响了。 这时再运行程序,程序的执行结果就是10w了:

 时间轴图:

加锁,本质上是把并发的变成了串行的。但加锁与join()有本质区别:join()是让两个线程完整地进行串行,加锁是两个线程的某个小部分串行了,其它的大部分都是并发的。 上述代码的所有步骤中,只有count++这一步是串行的,其它的操作如创建变量i,判定条件,调用add()和add()返回等操作全是并行的。

可见,加锁会导致阻塞。代码阻塞对程序的效率是有一定的影响的。此处加了锁,要比不加锁更慢一些,但肯定要不串行更快;同时也比不加锁算得更准。

(3)synchronized修饰方法

修饰普通成员方法

如果直接用synchronized修饰成员方法,这就相当于以this为锁对象:

修饰静态成员方法 

如果synchronized修饰静态方法(static,即类方法),此时就不是给this加锁了,而是给类对象加锁。

补充:这里的类对象指的是 Counter.class。我们在.java源代码文件中编写的代码被javac编译为.class(二进制字节码文件)后,就可以被JVM执行了。而JVM要想执行这个.class文件,就得先把该文件的内容读取到内存中。这个将.class文件内容读取到内存中的操作叫做类加载。 

在内存中,用类对象来表示.class文件内容。在.class文件中,描述了类的方方面面的详细信息,包括但不限于:1、类的名字;2、类有哪些属性,属性的名字、类型、访问权限;3、类有哪些方法,方法的名字,参数、类型、访问权限;4、类继承自哪个类;5、类实现了哪些接口……

此处的类对象,就相当于是“实例的图纸”。有了这个图纸,才能了解到这个实例是啥样的,进一步地才可以使用反射 API 来获取这里的一些信息。

(4)手动指定一个锁对象

更多时候,还是由我们自己来手动指定一个锁对象:

synchronized()括号中写什么都行,只要是一个Object实例。事实上,锁对象没有什么特别的,就是一个吉祥物,唯一的作用还是这句话:如果多个线程针对同一个对象加锁,就会产生锁竞争;多个线程针对不同的对象加锁,就不会有锁竞争。锁对象仅仅是起到一个标识的效果。

2、volatile 关键字:能保证内存可见性

(1)volatile 修饰的变量能够保证其 “内存可见性”。

volatile的读音:https://fanyi.baidu.com/#en/zh/volatile

在上面引起线程不安全的原因中,提到了内存可见性问题:例如在下面的代码中,由于t1线程中while循环的转速极快,而将flag变量load进内存这一指令步骤耗费了主要开销,于是编译器就将load这一步骤优化掉了。这样一来,t1线程中的flag并不是每次循环都从内存中读取的,而是第一次从内存中读取到的值;且t2线程中对flag作出的更改在t1线程中感知不到。因此,该程序的运行结果并不会符合我们的预期。

// 预期:在t2线程中输入一个非0的数,t1线程中循环终止

public class Test2 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 0) {
                // 空着
            }
            System.out.println("循环结束!t1结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner reader = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = reader.nextInt();
        });
        t1.start();
        t2.start();
    }
}

// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

而如果一个变量被 volatile 修饰,那么此时编译器就会禁止上述优化。换句话说,volatile 关键字能保证每次都从内存重新读取数据。

我们给flag加上volatile关键字修饰,再次运行上述代码。这时,代码的bug解决了,程序能够按照预期执行:

在加上volatile之前,由于t1线程中编译器优化掉了对flag的load这一指令,因此我们在t2线程中对flag作出的更改,在t1中感知不到。而加上了volatile之后,它能保证每一次while循环的条件判断都重新读取内存中flag的值,那么t2中对flag的修改在t1中能够立即感知到,这样一来t1的循环就能正确退出。 

给flag变量加上volatile,能恢复正常情况

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存),速度非常快,但是可能出现数据不一致的情况。而加上 volatile , 强制读写内存, 速度是慢了,但是数据变的更准确了。这也印证了数据的准确性和程序效率往往不能兼得。

(2)volatile 不保证原子性

注意:volatile synchronized 有着本质的区别。synchronized 能够保证原子性,而volatile 保证的是内存可见性,与原子性无关。

*至于 synchronized 是否也能保证内存可见性,是众说纷纭,存在争议的。

结论:

volatile适用于一个线程频繁读,一个线程写的情况。

synchronized适用于多个线程写的情况。

3、volatile关键字:禁止指令重排序

上面引起线程安全问题的原因中,提到了因为编译器优化造成指令重排序而导致的问题:

volatile关键字可以禁止指令重排序。如果用volatile关键字修饰s,那么创建对象时候就会禁止指令重排序,就能够保证执行顺序是 1->2->3 了。(PS:这个场景通过加锁也可以解决问题。)

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

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

相关文章

【边缘计算】登临(Goldwasser-UL64)BW-BR2边缘设备配置指南

目录 开箱配置激活SDK环境测试cuda兼容性 开箱配置 更改盒子root用户密码&#xff1a; sudo passwd root(密码同为root) 切换到root用户身份&#xff1a; su root查看ssh的状态&#xff0c;没有返回说明没有启动 sudo ps -e|grep ssh此时说明ssh服务已启动。 更改ssh配置文…

Android MediaCodec dump MP4实践小结

1.应用背景 在一些集成了算法SDK的Android APP中&#xff0c;这些APP是取出摄像头实时帧&#xff0c;然后调用视觉算法SDK并产生检测结果。而当测试人员发现某一场景下算法效果欠佳时&#xff0c;需要从摄像头实时原始数据帧dump一段视频&#xff08;mp4格式&#xff09;&#…

【TB作品】MSP430单片机 Proteus仿真 DS18B20温度 LCD1602显示器 温度读取与显示

效果图如下&#xff1a; 首先,让我们先来说说DS18B20集成电路。 • DS18B20是一款采用OneWire通讯协议的集成电路,因此只需要一条线就可以与微控制器通讯。它不需要额外的电源,但是也有外部电源输入端口。 • OneWire设备具有64位的ROM代码。如我们之前所说,这64位的前8位是家…

HDCTF

Welcome To HDCTF 2023 看源码找到game.js 找到这一串 放到控制台运行即可 SearchMaster 题目让post提交一个data 随便传一个在页面执行了 当传入{时他会报错&#xff0c;看报错信息发现 Smarty&#xff0c;猜测Smarty的ssti&#xff0c;数据发送到前端 用{if}标签即可 {…

CHAPTER 5: 《DESIGN CONSISTENT HASHING》 第5章 《设计一致的哈希》

CHAPTER 5: DESIGN CONSISTENT HASHING 为了实现水平扩展&#xff0c;有效且均匀地分发请求/数据是很重要的在服务器上。一致散列是实现这一目标的常用技术。但首先&#xff0c;让我们深入了解一下这个问题。 重组问题 如果您有n个缓存服务器&#xff0c;那么平衡负载的常用…

PCB射频电路四大基础特性及设计技巧

由于射频(RF)电路为分布参数电路&#xff0c;在电路的实际工作中容易产生趋肤效应和耦合效应&#xff0c;所以在实际的PCB设计中&#xff0c;会发现电路中的干扰辐射难以控制。 如&#xff1a;数字电路和模拟电路之间相互干扰、供电电源的噪声干扰、地线不合理带来的干扰等问题…

catkin_make_workspace

ERROR1 : CMake Error at /opt/ros/melodic/share/cv_bridge/cmake/cv_bridgeConfig.cmake:113 (message): Project ‘cv_bridge’ specifies ‘/usr/include/opencv’ as an include dir, which is not found. It does neither exist as an absolute directory nor in ‘${{pr…

Vue-列表过滤

列表过滤 对已知的列表进行数据过滤(根据输入框里面的内容进行数据过滤) 编写案例 通过案例来演示说明 效果就是这样的 输入框是模糊查询 想要实现功能&#xff0c;其实就两大步&#xff0c;1获取输入框内容 2根据输入内容进行数据过滤 绑定收集数据 我们可以使用v-model去…

6.3 收敛性与稳定性

6.3.1 收敛性 数值计算方法的收敛性是指&#xff0c;当取步长趋近于零时&#xff0c;数值解趋近于精确解的速度。一般来说&#xff0c;数值计算方法的收敛性是判断其优劣的重要指标之一。 数值计算方法的收敛性可以通过数学分析来研究&#xff0c;一般需要对数值解和精确解之…

08-Node.js—nvm

目录 1、介绍2、使用2.1 下载安装2.2 常用命令2.2.1 nvm list available2.2.2 nvm list2.2.3 nvm install 18.12.12.2.4 nvm install latest2.2.5 nvm uninstall 18.12.12.2.6 nvm use 18.12.1 参考 1、介绍 nvm 全称 Node Version Manager 顾名思义它是用来管理 node 版本的工…

系统集成项目管理工程师——考试重点(三)项目管理一般知识

1.项目定义&#xff1a; 为达到特定的目的&#xff0c;使用一定资源&#xff0c;在确定的期间内&#xff0c;为特定发起人提供独特的产品、服务或成果而进行的一系列相互关联的活动的集合。 2.项目目标&#xff1a; 成果性目标&#xff1a;项目产品本身 约束性目标&…

频繁GC引起卡顿问题排查与解决

一 问题描述 今天测试组更新测试环境后发现系统卡顿&#xff0c;无法办理任何业务&#xff0c;重启系统后问题仍然存在。已经到项目后期&#xff0c;迭代测试时间十分紧张。此问题直接影响到项目进度 二 排查过程 1.执行命令top Linux 下常用top命令显示系统中各个进程的资…

PID原理

PID控制器&#xff08;比例-积分-微分控制器&#xff09;&#xff0c;由比例单元&#xff08;P&#xff09;、积分单元&#xff08;I&#xff09;和微分单元&#xff08;D&#xff09;组成。 可以通过调整这三个单元的增益Kp&#xff0c;Ki和Kd来调定其特性&#xff0c;PID控制…

PowerDesigner 15 安装、汉化、逆向生成ER图、物理模型转逻辑模型、生成sql及简单使用

文章目录 前言PowerDesigner 15 安装、汉化、逆向生成ER图、物理模型转逻辑模型、生成sql及简单使用1. 安装2. 汉化3. 使用4. 逆向生成ER图4.1. 创建新模型4.2. 根据sql逆向生成er图 5. 物理模型新建表6. 物理模型转逻辑模型7. 生成sql语句 前言 如果您觉得有用的话&#xff0c…

Modelsim10.7仿真报错

把之前老版本的modelsim换掉了&#xff0c;新的装好仿真发现有点小毛病&#xff0c;记录以下 使用modelsim10.7仿真时出现错误&#xff0c;编译通过但报以下错误 ** Note: (vsim-3812) Design is being optimized…** INTERNAL ERROR: vopt returned success but vsim could …

arthas的简单使用

目录 arthas是什么为什么要使用arthasarthas能做什么安装arthas前提准备arthas主要命令trace命令watch命令monitor命令jad命令dashboard命令Thread命令sc命令mc命令redefine命令 实战演练1.定位到需要修改的类2.将定位到的.class文件反编译成.java文件3.修改.java文件4.将修改后…

不同局域网下使用Python自带HTTP服务进行文件共享「端口映射」

文章目录 1. 前言2. 视频教程3. 本地文件服务器搭建3.1 python的安装和设置3.2 cpolar的安装和注册 4. 本地文件服务器的发布4.1 Cpolar云端设置4.2 Cpolar本地设置 5. 公网访问测试6. 结语 转载自内网穿透工具的文章&#xff1a;Python一行代码实现文件共享【内网穿透公网访问…

Spring Bean生命周期源码详解

文章目录 Bean生命周期源码生成BeanDefinitionSpring容器启动时创建单例Bean合并BeanDefinitiongetBean()方法加载类实例化前实例化BeanDefinition的后置处理实例化后依赖注入执行Aware回调初始化前初始化初始化后销毁逻辑 Bean生命周期源码 我们创建一个ApplicationContext对…

深度学习:神经网络的前向传播过程

Author:龙箬 Computer Application Technology Change the World with Data and Artificial Intelligence ! CSDNweixin_43975035 哲学与爱情是我永远都搞不懂的两件事情 注&#xff1a; 以三层神经网络为例说明神经网络的前向传播过程激活函数采用 R e L U ReLU ReLU 函数 w…

搞懂位图和布隆过滤器

文章目录 位图腾讯面试题位图概念位图实现位图的应用位图的应用题 布隆过滤器布隆过滤器提出布隆过滤器概念布隆过滤器实现原理布隆过滤器的应用场景如何选择哈希函数个数和布隆过滤器长度 - - 目的减少误判率布隆过滤器的实现布隆过滤器优点布隆过滤器缺陷 海量数据面试题哈希…