【多线程】深入剖析线程安全问题

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


前言

线程安全问题是在多线程学习中一个十分重要的话题。多个线程并发执行就容易产生许多冲突与问题,如何协调好每个线程的执行,让多线程编程“多而不乱”,就是线程安全问题学习所要实现的了。这篇文章就让我们来深入探讨线程安全吧

目录

前言

一、概念

二、Synchronized

1、修改共享数据问题

2、解决方法

3、synchronized使用

(1)修饰代码块

(2)修饰方法

4、synchronized特性

(1)互斥

(2)可重入

三、死锁

1、循环依赖

2、哲学家问题

四、volatile

1、内存可见性

2、volatile


一、概念

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:
        如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

        造成线程不安全的主要原因是线程调度是随机的,这是线程安全问题的罪魁祸⾸,随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数. 程序猿必须保证 在任意执⾏顺序下 , 代码都能正常⼯作.

二、Synchronized

1、修改共享数据问题

让我们先来看一下下面这段代码:

public class Main {public 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);}
}

如果是在单线程的环境下执行类似逻辑,那结果毫无疑问肯定是100000,但是实际结果却相距甚远:

甚至每次运行的结果都不一样:

上⾯的线程不安全的代码中, 涉及到多个线程针对 count 变量进⾏修改. 此时这个 count 是⼀个多个线程都能访问到的 "共享数据"。多线程在同时修改同一个数据时就容易出现问题。
主要是由于修改操作看似是一个操作,实际上操作系统要执行多个指令。比如在执行count++操作时就有三步操作:
1、load   把内存中的数据读取到CPU寄存器中
2、add    把cpu寄存器里的数据+1
3、save   把CPU寄存器里的值写回内存

而CPU在调度执行线程时,随时都有可能切换执行其它线程(抢占式执行,随机调度)

指令是CPU执行的最基本单位,要切换线程,也会等当前线程的指令执行完毕才调走,不会出现指令执行一半的情况。

但由于count++操作需要三个指令,CPU执行了一个指令或两个指令或三个指令的任何时候都有可能被调度走从而使此次count++操作的结果产生偏差,并且由于调度的随机性,这种偏差也是无法预测的,也就导致了我们上面所看到的每次程序执行结果都不同的现象了

还有很多可能得情况,博主就不一一例举了。但只有第一二种情况程序才能正常运行:

这样两个线程的三个指令都能分别完整的被执行完,最后结果就会是count=2了。

错误的情况会类似下面这种:

上述过程中,明明执行了++操作两次,但最终结果却是1。因为这两次加的过程中结果出现了覆盖。

由于五万次循环中,无法确定有多少次的执行顺序是1、2这两种正确的执行顺序,因此最终的结果是不确定的,而这个值是一定小于10万的。

2、解决方法

会出现上述问题的主要原因是java中许多语句的执行都不是原子性的,这导致了如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。

比如说我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。

那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。

同样的,我们也可以给线程上一把“锁”,把“非原子”的操作变成“原子”,保证线程执行的原子性。在java中,我们就可以用synchronized实现该操作

3、synchronized使用

(1)修饰代码块

用sychronized修饰代码块{},进入{就会自动加锁,出了}就会解锁,如下:

这时我们发现程序报错了。是由于()中需要指定一个锁对象,这个锁对象可以是任何对象,重点是通过锁对象的比较来确定两个线程是否是否要上同一把锁,如果锁对象一致,就会产生锁竞争,只有一个线程执行完毕解锁后,其它竞争线程才能拿到锁执行操作。因此我们应该为这两个线程准备一个锁对象:
public class Main {public static int count=0;public 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);}
}

这下程序结果就没问题了:

运行流程图:

本质上就是把随机的并发执行过程,强制变成了串行,从而解决了该线程安全问题

注意:
  • 此处的加锁,并不是真的让count++变成原子的,也没有干预线程的调度,只不过是通过这种加锁的方式,使一个线程在执行count++的过程中,其它的线程的count++不能插队进来
  • synchronized是关键字,不是方法,()中的并不是参数,需要指定一个“锁对象”,可以是任何对象,来进行后续判定
  • 锁对象的作用就是来区分多个线程是否针对“同一个对象”加锁,是通过同一个对象加锁,就会出现阻塞等待锁竞争/锁冲突),若不是则不会出现“阻塞”,线程间任然是随机调度的并发执行
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产⽣竞争

(2)修饰方法

synchronized不仅可以修饰代码块,还能修饰方法:

class Method{public static int count=0;public synchronized void add(){//等价于synchronized (this){}count++;}//修饰静态方法
//    public static synchronized void add2(){
//        //等价于synchronized (Method.class){}
//        count++;
//    }}
public class Main {public static void main(String[] args) throws InterruptedException {Method method=new Method();Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {method.add();}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {method.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Method.count);}
}
synchronized用的锁存在java对象头里面的,在一个java对象中,除了自己定义的属性和方法,还有一些自带的属性,这些自带的属性就称为对象头,其中就有属性表示当前对象是否加锁。

 注意

并非加了sychronized就一定线程安全,还是看具体代码的执行。是否要synchronized,如何加锁都是与应用场景直接相关的。锁在需要的时候才使用,不需要的时候尽量不要“无脑加锁”,因为上锁是需要消耗资源的,无意义的上锁反而会降低运行效率

并且,使用锁就有可能触发阻塞,阻塞的时长,何时可以恢复执行都是不可控的,“无脑加锁”反而可能引起其它问题

因此,实际开发中一般都是结合具体场景自行加锁,很少会直接用synchronized修饰方法。像java中自带的StringBuffer、Vector、Hashtable都不推荐使用,甚至jdk在未来版本中还可能直接把他们优化掉


4、synchronized特性

(1)互斥

synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待.
进⼊ synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

synchronized⽤的锁是存在Java对象头⾥的。

可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表⽰当前的 "锁定" 状态(类似于厕所的 "有⼈/⽆⼈").
如果当前是 "⽆⼈" 状态, 那么就可以使⽤, 使⽤时需要设为 "有⼈" 状态.
如果当前是 "有⼈" 状态, 那么其他⼈⽆法使⽤, 只能排队

理解 "阻塞等待".
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进⾏加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁.
注意:
上⼀个线程解锁之后, 下⼀个线程并不是⽴即就能获取到锁. ⽽是要靠操作系统 "唤醒". 这也就是操作系统线程调度的⼀部分⼯作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B ⽐ C 先来的, 但是 B 不⼀定就能获取到锁, ⽽是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized的底层是使⽤操作系统的mutex lock实现的

(2)可重入

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
理解 "把⾃⼰锁死":

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

这时我们发现:

(1)里面的synchronized想要拿到锁,就需要外面的synchronized释放锁

(2)外面的synchronized想要释放锁,就需要执行到“}”

(3)想要执行到“}”就需要执行完add()方法

(4)但此时add()方法正处于阻塞等待中

这就导致了代码执行陷入死循环,程序会不断阻塞下去

这样的锁称为 不可重⼊锁
Java 中的 synchronized 是 可重⼊锁, 因此没有上⾯的问题.
在可重⼊锁的内部, 包含了 "线程持有者" "计数器" 两个信息.

  • 可重入锁加锁前是要判断当前这个锁是否是被占用的状态
  • 加锁时会在锁中额外记录当前是哪个线程对这个锁加锁了,即记录“线程持有者”
  • 如果发现加锁线程就是当前锁的持有者,并不会真正地进行加锁操作,也不会进行任何阻塞操作,而是直接放行,往下继续执行代码,同时锁内的计数器值会加一
  • 每当执行到一次“}”时,即多重锁中某一个锁要“解锁”时,则计数器值减一,判断当前计数器值是否为 0 ,若不是,继续执行后续代码;若是,才真正释放锁(才能被别的线程获取到)

三、死锁

出现死锁的第一种情况就是上述的,一个线程针对一把锁连续加锁两次的情况,这是不可重入锁会出现的问题,不过java中的synchronized是可重入锁,因此这种情况就不必讨论了。

1、循环依赖

我们先来例举一个场景

公司规定要进入办公楼需向保安出示工牌,但公司高管a把工牌落在公司内了,   a要要去办公楼工作,却被保安因没有工牌而拦下了,a说他得进去才能拿到工牌,保安则说没有工牌就不能进去,如果二者互不相让,就会一直僵持在这。

当两个人的工作互相依赖于对方工作的完成才能完成的话,就会陷入这种死循环的局面

多线程中也是这样:

有两个线程1和2,两把锁A和B,线程1先针对A加锁,线程2针对B加锁,线程1在不释放锁A的情况下再针对B加锁,同时线程2在不释放B的情况下针对A加锁,双方都得等到对方占有的锁解开才能解开自己的锁,就会导致两个线程一直处于“阻塞”状态

public class Main {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) {throw new RuntimeException(e);}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) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2 加锁 locker1 完成");}}});t1.start();t2.start();t1.join();t2.join();System.out.println("主线程运行完毕");}
}

执行结果

通过jconsole可以直接观测到两个线程的状态:

2、哲学家问题

题目描述:

有五个哲学家围坐在一个圆桌旁,每个人面前有一盘面,每两个人之间放了一根筷子,所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的筷子才能吃到面,而同一根筷子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把筷子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的筷子,但在没有同时拿到左右筷子时不能进食

通常情况下,这个模型是可以正常运转的,但一旦出现极端情况,就会出现死锁。

例如五位哲学家同时拿起了左手边的筷子,他们的右手都没有筷子可拿,且哲学家非常固执,吃不到面就绝对不会放下左手的筷子。这就导致了每个哲学家手上都有且仅有一根筷子,而只有一根筷子又无法完成吃面操作,就会等待其它哲学家吃完面后放下的筷子,而每个哲学家都没法吃面也不会放下筷子,就导致所有的哲学家都会一直陷入“阻塞等待”的状态,也就出现了“死锁”

这时我们就需要去研究一下出现死锁的必要条件了:

1、锁是互斥的【锁的基本特性】

2、锁是不可被抢占的,线程1拿到了锁A,如果线程1不主动释放A,线程2不能把A抢过来

3、请求和保持。线程1 拿到锁 A 后,在不释放 A 的情况下,去拿锁 B

4、循环等待/环路等待/循环依赖,多个线程获取锁的过程存在循环等待

1和2是由synchronized锁的基本特性导致的,程序员无法去干预,所以一般不从这两个方面去试图解决死锁。

针对3、4,如果必须按照请求和保持的方式,获取N个锁,该如何避免出现循环等待呢?

我们可以给每个锁编号1,2,3……,约定所有线程在加锁的时候,必须按照一定的顺序来加锁,比如必须先针对编号小的锁加锁,再对编号大的锁加锁:

如上图,哲学家5吃完面后就能放下4、5两根筷子,哲学家4就能拿起筷子4,就能用筷子3、4吃上面,吃完后又会放下筷子3、4,让哲学家3也能吃上面,依次类推……这样,每个哲学家就都能吃上面,不会出现一开始的“循环等待”

也就是说,我们可以这样改造之前的代码,让t1和t2都按照先加小锁再加大锁的顺序加锁,就不会出现死锁了:

public class Main {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) {throw new RuntimeException(e);}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) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2 加锁 locker2 完成");}}});t1.start();t2.start();t1.join();t2.join();System.out.println("主线程运行完毕");}
}

 只要遵守一定的加锁的顺序,无论接下来该模型的运行顺序如何,无论出现多么极端的情况,都不会再出现“死锁”


四、volatile

1、内存可见性

我们先来看一下下面这段代码:

public class Main {public static int n=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (n==0){//什么都不写}System.out.println("t1 线程结束循环");});Thread t2=new Thread(()->{Scanner in=new Scanner(System.in);System.out.println("请输入一个整数:");n=in.nextInt();});t1.start();t2.start();}
}

按道理来说在我们输入一个非 0 整数后,n的值就不为0了,t1线程中的while(n==0)的判断就为假,跳出while循环,然后输出“t1 线程结束循环”语句,但实际运行却不是这样:

可以看到,在输入1后,t1并没有像我们预想的那样输出任何语句,通过jconsole我们也可以看到t1线程(Thread-0)任然在持续地工作:

很明显实际运行结果与我们预期的结果并不相符,这就是出现“bug”了,同样也是线程安全问题。

那么为什么会有内存可见性问题呢?让我们先计算机数据存储的构成:

而问题就出在t1线程中的这段代码:

while循环会循环非常多次,每次循环,都要执行一次“n==0”的判定,这次情况下实现这个判定需要两个操作:

(1)从内存中读取数据到寄存器

        (读取内存,这个操作相对而言速度非常慢)

(2)通过类似于cmp的指令,比较寄存器0的值

        (这个指令的执行速度相对就非常快了)

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

这样,在后续运行时循环的开销确实大幅降低了。但是,一旦当用户修改n的值时,内存中的n值会发生改变,但寄存器中的n值没有变化,又由于此时循环不会真的读取内存,也就感知不到n的变化。

这时,内存中的n值的变化,对于t1来说就是“不可见的”。这就导致bug的产生,也就是“内存可见性问题”

那么,编译器为何要做出这种优化呢?

主要是由于一些程序员写出的代码过于低效,为了降低程序员的门槛,即使代码水平一般,最终的运行速度也不至于太低,因此,主流编译器都会引入优化机制

优化编译器会自动调整你的代码,保持在原有逻辑不变的前提下,提高代码的执行效率。在一般情况下,代码优化的效果是非常好的。但是,编译器的优化是一个非常复杂的问题,某个代码,何时优化,优化到什么程度都是不好确定的。对于程序员来说,很难确定某个代码是否会优化,并且代码稍微变化一点,优化结果可能都截然不同,比如当我们稍微修改一下t1线程的代码:

此处即使sleep的时间非常短,刚才的内存可见性问题就消失了,此时t2对于n的修改,t1就可以感知到了,运行结果也就正常了:

这说明加入sleep后,刚才谈到的针对读取n内存数据优化的操作就不再进行了,因为和读取内存操作相比,sleep的开销是更大的,远远超过了读取内存的开销。此时再对内存读取做优化对效率的提升就微乎其微了,编译器也就不会优化了。

如果这时候,循环里并没有sleep,我们又希望代码能够无bug地正常运行,这是我们就可以利用“volatile”关键字了。

2、volatile

volatile 修饰的变量,会提示编译器:这个变量是“易变”的。编译器做出上述优化的前提是,编译器认为针对某个变量的频繁读取,结果都是固定的。而“易变”的就是告诉编译器该变量值未来是可能会发生变化的,此时编译器就会禁止上述优化,保证每次循环都是从内存重新获取的数据, 从而不会出现 "内存可⻅性"问题了。

代码在写⼊ volatile 修饰的变量的时候,
  • 编译器生成代码时,会在变量读取操作附件生成一些特殊指令,称为“内存屏障”,后续JVM执行到这些特殊指令,就不会进行上述优化了
  • 改变线程⼯作内存中volatile变量副本的值
  • 将改变后的副本的值从⼯作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
  • 从主内存中读取volatile变量的最新值到线程的⼯作内存中
  • 从⼯作内存中读取volatile变量的副本
前⾯我们讨论内存可⻅性时说了, 直接访问⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度⾮常快, 但是可能出现数据不⼀致的情况.
加上 volatile , 强制读写内存. 速度是了, 但是数据变的更准确了.
因此只需要用volatile修饰变量n,告诉编译器n是“易变”的,这样程序就能正常执行了
public class Main {public volatile static int n=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (n==0){//什么都不写}System.out.println("t1 线程结束循环");});Thread t2=new Thread(()->{Scanner in=new Scanner(System.in);System.out.println("请输入一个整数:");n=in.nextInt();});t1.start();t2.start();}
}

编译器的开发者知道在某些场景下,优化可能会出现bug,于是就通过“volatile”这类关键字把权限交给了程序员,让程序员可以部分干预优化的进行,把优化权交给程序员,从而尽可能减少类似“内存可见性”的问题

注意:

volatile 不保证原⼦性

如果多个线程对同一个变量执行修改操作(count++),volatile也无能为力。该加锁还是要加锁


那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

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

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

相关文章

【Node】【3】回调函数

nodejs 是一个基于事件驱动和非阻塞异步的JavaScript运行时环境。 Node.js 采用单线程模型&#xff0c; 单线程意味着 Node.js 在任何给定时刻只能执行一段代码&#xff0c;但通过异步执行回调函数&#xff0c;可以在等待 I/O 操作完成的同时继续执行其他代码&#xff0c;从而…

每日一练-threejs实现三维动态热力图

前言&#xff1a;学习自用Three.js搞个炫酷热力山丘图&#xff0c;作者讲解的十分详细&#xff0c;在这里不再过多赘述&#xff0c;直接上代码&#xff01; <template><div class"map" ref"map"></div> </template><script set…

XTuner微调个人小助手认知 #书生浦语大模型实战营#

1.任务&#xff1a; 本次的任务是使用 XTuner 微调 InternLM2-Chat-1.8B 实现自己的小助手认知&#xff0c;从而让模型能够个性化的回复&#xff0c;让模型知道他是我们的小助手&#xff0c;在实战营帮我们完成XTuner微调个人小助手认知的任务。并截图打卡。 任务打卡&#x…

书生.浦江大模型实战训练营——(十一)LMDeploy 量化部署进阶实践

最近在学习书生.浦江大模型实战训练营&#xff0c;所有课程都免费&#xff0c;以关卡的形式学习&#xff0c;也比较有意思&#xff0c;提供免费的算力实战&#xff0c;真的很不错&#xff08;无广&#xff09;&#xff01;欢迎大家一起学习&#xff0c;打开LLM探索大门&#xf…

复杂的编辑表格

需求描述 表格可以整体编辑&#xff1b;也可以单行弹框编辑&#xff1b;且整体编辑的时候&#xff0c;依然可以单行编辑 编辑只能给某一列&#xff08;这里是参数运行值&#xff09;修改&#xff0c;且根据数据内容的参数范围来判断展示不同的形式&#xff1a;input/数字输入/单…

计算机网络——TCP协议与UDP协议详解(下)

一、TCP协议 1.1 TCP协议的报文 TCP全称为 "传输控制协议(Transmission Control Protocol")。人如其名&#xff0c;要对数据的传输进行一个详细的控制。我们先看其报文格式&#xff0c;如下图&#xff1a; TCP报文由以下几个字段组成&#xff1a; 源端口号和目标端口…

MySQL索引详解:原理、数据结构与分析和优化

在数据库管理系统中&#xff0c;索引是提高查询性能、优化数据存储结构的重要工具。MySQL作为广泛使用的开源关系型数据库管理系统&#xff0c;其索引机制对于提升数据库操作效率具有至关重要的作用。本文将围绕“MySQL索引详解&#xff1a;原理、数据结构与分析和优化”这一主…

CRUD的最佳实践,联动前后端,包含微信小程序,API,HTML等(二)

CRUD老生常谈&#xff0c;但是我搜索了一圈&#xff0c;发觉几乎是着重在后端&#xff0c;也就是API部分&#xff01; 无外乎2个思路 1.归总的接口&#xff0c;比如一个接口&#xff0c;实现不同表的CRUD 2.基于各自的表&#xff0c;使用代码生成器实现CRUD 个人来说是推荐2&am…

Harmony鸿蒙应用开发:解决Web组件加载本地资源跨域

鸿蒙开发文档中有一节 加载本地页面 提到了可以通过 $rawfile 方法加载本地 HTML 网页&#xff1a; Index.ets 1Web({ src: $rawfile("local.html"), controller: this.webviewController })但是如果在 local.html 中需要引用一些静态资源&#xff0c;例如图片、JS、…

MMS论文中关于语种识别的内容摘要

MMS论文中关于语种识别的内容摘要 前言语种识别相关内容实验结论 前言 摘要翻译一些内容。 论文地址请看这里 语种识别相关内容 Whisper支持LID&#xff0c;可以区分99种不同的语言&#xff1b;有人使用wav2vec 2.0实现LID&#xff0c;数据集中包含10种亚洲语言&#xff1b;…

JavaScript - Ajax

Asynchronous JavaScript And XML&#xff0c;异步的JavaScript和XML 作用: 数据交换&#xff1a;通过Ajax可以给服务器发送请求&#xff0c;并获取服务器响应的数据。异步交互&#xff1a;可以在不重新加载整个页面的情况下&#xff0c;与服务器交换数据并更新部分网页的技术…

[新手入门]1台电脑+1个电视+2个软件(sunshine+moonlight) 解决黑神话悟空没有hdmi线的痛...

sunshinemoonlight 解决黑神话悟空 本地串流投屏 背景:偶然间在B站发现了sunshinemoonlight方案,替代hdmi线,做本地串流...于是心灵手巧的我开始尝试踩坑之路:1.准备安装包2.开始安装2.1 笔记本windows安装sunshine2.2 遇到了第一个坑.Fatal: ViGEmBus is not installed or run…

无需多部备用机,云手机方便又便宜!

云手机&#xff0c;是云计算技术的又一创新应用&#xff0c;它通过在云服务器上虚拟出带有原生安卓操作系统的手机实例&#xff0c;为用户提供了一种全新的手机使用体验。无需携带多部手机&#xff0c;只需通过云手机&#xff0c;便可轻松实现多账号管理、应用运行及数据存储等…

【物理学】什么是运动学和动力学?

Kinematics 和 Kinetics 是力学中的两个重要分支&#xff0c;它们虽然都涉及物体的运动&#xff0c;但关注的方面不同。 Kinematics&#xff08;运动学&#xff09; Kinematics 主要研究物体的运动&#xff0c;而不涉及导致运动的力。它关注的是运动的几何特性&#xff0c;比…

基于SSM的学生管理系统的设计与实现(包含源码、sql脚本、导入视频教程)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSM的学生管理系统2拥有三种角色 管理员&#xff1a;学生管理、教师管理、课程管理、个人信息管理等教师&#xff1a;添加课程、录入成绩、查看选课名单和结课、个人信息等学生&…

i.MX6裸机开发(11)——DDR测试

本章参考资料&#xff1a;《IMX6ULRM》(参考手册)。 学习本章时&#xff0c;配合《IMX6ULRM》Chapter 33: Multi Mode DDR Controller (MMDC) 一起阅读&#xff0c;效果会更佳&#xff0c;特别是涉及到寄存器说明的部分。 特别说明&#xff0c;本书内容是以i.MX6U系列控制器资…

SSRF漏洞实现

目录 ssrf简介SSRF(Server-Side Request Forgery:服务器端请求伪造) SSRF题1 前期介绍 方法1&#xff1a;ssrfredis写入webshell 扫ip&#xff1a;端口 使用工具写木马 SSRF题2 ssrffastcgi未授权访问写入webshell 环境搭建&#xff1a; 攻击&#xff1a; ssrf简介 SS…

UE5学习笔记18-使用FABRIK确定骨骼的左手位置

一、在武器的骨骼资产中创建一个新的插槽 二、在动画类中添加代码 xxx.h UPROPERTY(BlueprintReadOnly, Category Character, meta (AllowPrivateAccess "true"))/** 蓝图只读 类型是Character 允许私有访问 */ FTransform LeftHandTransform;//拿武器时知道左手…

【数模资料包】最新数模国赛word+latex模版|数模常用的算法python+matlab代码

【2024最全国赛研赛数模资料包】C君珍贵国一数模资料&#xff5c;最新数模国赛wordlatex模版&#xff5c;数模常用的算法pythonmatlab代码 国赛指&#xff1a;高教社杯全国大学生数学建模竞赛&#xff0c;研赛指&#xff1a;华为杯研究生数学建模竞赛。资料内容具体看文末卡片…

Java:BigDecimal 解决小数运算失真问题

文章目录 BigDecimal代码 BigDecimal 解决小数运算失真问题 解决方法&#xff1a;转换为BigDecimal对象 代码 package com.zhang.math;import java.math.BigDecimal;/*** Author: ggdpzhk* CreateTime: 2024-08-25*/ public class BigDecimalTest {public static void main(…