【JavaEE】线程安全与线程状态

作者主页:paper jie_博客

本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。

本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将MySQL基础知识一网打尽,希望可以帮到读者们哦。

其他专栏:《MySQL》《C语言》《javaSE》《数据结构》等

内容分享:本期将会分享线程安全与线程状态~

目录

 线程状态

线程的所有状态

状态的意义

状态图

查看状态

线程安全

什么是线程安全

经典栗子

原因

导致线程不安全的原因

解法方法

加锁 - synchronized

加锁如何操作

加锁后的代码

注意

内存可见性问题

经典栗子

原因

解决方法

Java中锁的特性

互斥性

可重入性

死锁

死锁问题的常见三种情况

解决方法

线程的通知等待 - wait和notify

wait方法

wait的使用

notify方法

注意

wait和sleep的区别

Java标准库中的线程安全类

线程不安全类

线程安全类


 线程状态

线程的所有状态

1. NEW Thread对象创建好了,但还没有调用start()去系统中创建线程

2. RUNNABLE 调用了start(),线程正在执行或者准备就绪随时准备被调度

3. TERMINATED Thread对象还在,但是系统中的线程已经执行完销毁了.

4. TIMED_WAITING 有时间现在的堵塞状态,到达一定时间会解除堵塞

5. WATING 死等的堵塞状态,需要达到一定的条件才会解除堵塞

6. BLOCKED 由于锁竞争引起的堵塞

状态的意义

状态存在的最大用处就是我们去调试多线程出现的bug时会给我们提供很大的参考意义.比如: 程序卡住了,那可能就是一些相关的线程进入了堵塞状态. start()一个Thread对象只能使用一次这是和NEW密切相关的,只有在NEW状态才能使用start(),使用start()后就进入了另一个状态.

状态图

查看状态

我们可以通过JDK的jconsole来去查看进程里的线程的状态和调用栈的情况.我们可以根据这个来观察线程是不是堵塞了,为什么堵塞,执行到哪行堵塞了.

public class ThreadDemo5 {public static void main(String[] args) {Thread thread = new Thread(() -> {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}

线程安全

什么是线程安全

一段代码不论是在单线程上还是在多线程上都可以通过执行,不会出现bug,这就是"线程安全".

一段代码再单线程上可以通过,但是在多线程上会出现bug,这就是"线程不安全"或者"线程安全问题"

经典栗子

public class ThreadDemo6 {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: " + count);}
}

上述这个代码,我们预期的结果是100000,但是我们运行后发现结果不是100000.

原因

这就需要我们站在硬件的角度来看软件了. 我们知道count++在cpu中实际是三条指令:

1. load 去内存中拿到count的值放到cpu中的寄存器中

2. add 将寄存器中的值+1

3.将寄存器中的结果放到内存中

这里如果是一个线程执行结果肯定是不会出错的.但多个线程,那线程的调度也是随机的.这些指令的先后顺序就会产出多种情况.有的是正确的,有的是错的:

这里列出了几种情况,但其实这样的情况有无数种:

这里两个 线程是并发还是并行我们都不知道,反正两个线程都有自己的PCB,有各自的上下文,互不干扰(各自一套寄存器里的值,互不干扰).

通过观察我们发现,知道一个线程的save没执行,另一个线程的load执行了的话,那这个结果就不对,使用正确的情况应该是一个线程的save需要先执行完才能执行另一个线程的load.

导致线程不安全的原因

1. 根本原因: 这是因为操作系统线程是被随机调度的,抢占式执行,这可能就是导致指令的执行顺序不同.

2. 代码结构: 多个线程同时改变一个变量. 这里多个线程改变不同变量,多个线程读一个变量,一个线程改变一变量是都不会造成线程安全问题的.

3. 直接原因: 代码没有具有原子性. 这里count++虽然只有一个代码,但其实它有三个指令.在执行到一半的时候可能会被调度走,其他的线程就有机可乘插队进来.这可能就会导致错误.这里我们可以将count++的多个指令理解为一个整体.需要全部执行完才能执行其他的指令.这样才具有原子性.

4. 内存可见

5. 指令重排序

解法方法

知道了这几个方面的原因我们就可以对症下药了:

第一个问题的随机调度是操作系统控制的,我们没法改变操作系统,我们无从下手.

第二个问题我们在写代码的时候需要注意代码的结构,避免出现多个线程同时改变一个变量的问题,但有的时候是无法避免的.

第三个问题我们可以通过加锁的方法来将需要执行的代码指令打包成一个整体,这样就具有原子性了.

加锁 - synchronized

加锁的目的就是为了将需要的代码打包成一个整体,令他们具有原子性.加锁的特点就是排他性,互斥性. 这里就是一个线程在执行加锁操作时,其他的线程是不能执行这个加锁对象里的代码的.

举个栗子:

这就像有一个厕所,多个滑稽需要上厕所,一个滑稽进去后将门关上其他滑稽进不来看不到就叫做加锁,上完厕所出去就叫做解锁.这时其他的滑稽才可以进来.

加锁如何操作

在加锁前,我们需要引入一个类对象,加锁和解锁都是依托这个类进行的.这个类对象可以是Object类或者是它的任意一个子类.加锁在Java中是一个关键字 - synchronized.它的括号里面放所对象,花括号里面就是加锁,花括号后就是解锁.

这里加锁的核心就是一个线程对一个所对象进行加锁了,其他的线程再对这个锁对象进行加锁就会导致堵塞.一直到前面的线程解锁才会解除堵塞.这里就是所谓的锁竞争造成的堵塞.

且我们需要知道原子性这个说法不够准确. 不是说加锁了这里里面的指令就一定会完成或者都不完成.它中途还是会被调度出去的.只是说第一个加锁的线程可以保证后面对这个所对象加锁的线程指令不会插队到第一个线程指令中间执行.并不是说不能调度出CPU.

加锁后的代码

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

我们可以发现这时的结果就是正确的了.

注意

这里需要注意几个点:

一个线程加锁,一个线程不加锁.或者不同的线程加不同的锁这都会造成线程安全问题.

需要加锁的线程的所对象必须是同一个.

这里this和类名.class也是可以作为所对象的.

this:

这里this就是直接指代的test.

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

类名.class:

这里因为java进程中的一个类只有一个类对象,这样不同的线程使用的还是同一个对象,锁竞争还是会存在.

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

内存可见性问题

内存可见性问题和JVM的代码优化息息相关. 一个线程读,一个线程写也可能导致线程安全问题.

经典栗子

while(flag == 0)

这里我们预期的是通过输入1来跳过t1线程的循环,但是我们发现循环并没有跳过,光标还在闪烁. 

原因

这里我们需要知道while(flag == 0) 这句代码其实有两条指令:

1. lode 将内存的中flag读取到CPU的寄存器中

2. 将寄存器中的值与0比较(条件跳转指令)

整体的情况就是t1线程和main线程启动,mian线程需要等待输入,这之间至少需要几秒.而在这几秒的过程中while()会执行几百亿次.

关键有两点:

1. 在这多次lode中,读取内存中的值都是一样的,没发生改变.

2. 读取内存比条件跳转的开销大很多

这就会导致在等待输入这几秒中,大量的循环比较,其中去读取内存,读到的值却没有改变.读取内存的开销又特别大.这就会让JVM怀疑这样的操作有必要嘛.它就有可能会将读取内存指令删除只用寄存器中的值. 这就导致main中改变了flag,但t1线程却没看到,这就是内存不可见.

解决方法

内存可见性是高度依赖JVM的代码优化的具体实现,代码改变一点,结果可能就不一样.为了保证绝对性,Java中就引入了volatile关键字.它的作用就是保证内存可见. 它可以强制代码不进行优化,就是强制读取内存.

Java中锁的特性

互斥性

互斥性就是一个线程获取了这把锁,另一个线程再尝试获取就需要等待,这里就是锁竞争造成的堵塞.这个特性就是用来解决线程安全问题的.

可重入性

可重入性就是一个线程再使用一把锁的前提下,在嵌套二次使用这把锁.在这种情况下不会让线程卡死.

举个栗子:

public class TreadDemo12 {public static void main(String[] args) {Object object = new Object();Thread thread = new Thread(() -> {synchronized(object) {synchronized (object) {//写代码System.out.println("hello word");}}});thread.start();}
}

在这个代码中,如果不使用可重入锁,就会卡死,进入死锁状态.在C++中就没有可重入锁,就会陷入死锁状态. 这种死锁情况就是: 在一个线程里使用锁的前提下,嵌套第二次再使用这个锁.就会发生第一次这个锁对象已经加锁了,则第二次使用锁对象就需要等待第一次解锁,但第一个解锁在第二次加锁的后面.这就导致线程卡死,进入了死锁状态.

这就是相当于你将钥匙忘在了被锁的房间里.

在Java中就不会发生. 因为Java中的锁是可重入的. 由于是同一个线程,在第二次加锁的时候,就会直接放行,不会造成堵塞. 而Java中的锁可以重入是因为锁里面有两个重要的属性: 加锁线程 和 计数器

加锁线程这个属性会记录加锁的线程是谁. 计数器初始值为0,加锁就会+1,解锁就会-1.

在第一次加锁时,加锁线程就记录这个线程.计数器+1. 第二次就会判断加锁的线程和持有锁线程是不是同一个,是就直接计数器++,不是就堵塞等待. 出第二次加锁的括号,计数器就-1, 出第一次加锁的括号再-1,当计数器为0时,就是解锁成功.

死锁

加锁是对多线程的线程安全问题的解决方式,但是加锁操作不恰当就是会出现死锁问题.

死锁问题的常见三种情况

1. 一个线程一把锁:

一个线程中在持有这把锁的前提下,第二次使用这把锁,这就会导致死锁.但在Java中不会出现.

2. 两个线程两把锁:

一个线程在持有A锁的情况下去尝试获取B锁,同时另一个线程在持有B锁的情况下尝试获取A锁.这就会导致死锁.

栗子:

public class ThreadDemo13 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized(A) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println("在A加锁后,加锁B");}}});Thread t2 = new Thread(() -> {synchronized(B) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized(A) {System.out.println("在B加锁后,加锁A");}}});t1.start();t2.start();}
}

这就相当于车钥匙放到了被锁的房间里,房间钥匙放到了被锁的车里.

N个线程M把锁:

哲学家就餐问题

这里有5个哲学家,5个筷子. 一个哲学家就餐时需要使用两个身边的筷子. 当1滑稽就餐时需要1和5筷子,这是5滑稽和2滑稽想就餐就需要等待了.这样虽然需要等待但是最后还是可以吃上面.但是这里有一个极端的情况就是:所有的滑稽同时拿起左手边的筷子,这时每个人都只有一个筷子,这时需要拿起第二个筷子时发现没有筷子了就需要等待.但是所有人都在等待,就没人吃到面放下筷子.这就是循环等待.

解决方法

在分析解决方法钱我们需要知道发生死锁有4个必要条件:

1. 互斥性: 一个线程使用锁,另一个就需要等待.

2. 不可抢占: 一个线程在使用锁时,另一个线程不能强行抢占,只能等它自动解锁.

3. 请求保持: 一个线程持有一把锁的前提下,尝试获取另一把锁.

4. 循环等待

发生死锁,这4个条件缺一不可.

知道了发生死锁的条件后,我们就可以对症下药.我们只需要破坏其中一个条件就可以解除死锁.

1和2是锁的基本特性,我们不能改变.3我们需要看情况而定,有的情况可以避免,有的情况不可以避免.

4是最容易改变的.我们可以制定规则: 指定获取锁的顺序,为每个锁编号,先获取小的锁,再获取大的锁.这样就不会发生循环等待了.

改变锁的循环等待有多种方式:

1. 增加一把锁

2. 减少一个线程.

3. 引入计数器,限制最多多少个线程同时获取锁

4.制定加锁顺序规则(最常用)

5. 银行家算法

线程的通知等待 - wait和notify

这里是通过引入wait与notify来在应用层面来改变线程执行的先后顺序.

操作系统中线程在内核中的调度是抢占式,随机调度的,这是不可改变的.这里我们就是在应用代码层面来让线程主动放弃CPU的调度,从而影响到线程执行的先后顺序. 也就是让执行条件没达到的线程先放弃CPU的竞争,让其他线程先执行,等到条件达到时再参与竞争.

这里举个栗子:

多个滑稽老哥去ATM上执行一些操作

没有wait和notify时: 1号老哥进去取钱,发现没有钱了,那他就会出来与其他滑稽老哥进行竞争进入ATM的机会,1号老哥可能又会竞争到,再进去取钱发现没有钱,又出来和它们竞争,这样可能会多次1号滑稽进去但又没有进行到有用的操作.

转换成代码:

while(true) {synchronized(....) {if(ATM有钱) {//取钱操作}else {//什么也不做}}}

有wait和notify时: 1号老哥进去取钱,发现没有钱了.那他会出来先不参与和它们老哥竞争进去的机会,而是等待其他老哥把钱存进去后再参与竞争,这样就减少了无效操作.

代码:

while(true) {synchronized(....) {if(ATM有钱) {//取钱操作}else {wait();}}}

画图理解:

对于上面第一种情况还是比较容易发生的, 1号滑稽拿到锁,处于RUNNABLE状态,其他线程处于WAITING 状态. 当1号滑稽解锁后再次竞争时,其他滑稽需要系统先唤醒在竞争,而1号滑稽不用唤醒可以直接竞争. 

wait方法

对于wait方法,它的内部会做三件事情:

1. 解锁.

2. 进入堵塞状态.

3. 等到其他线程执行到notify方法时,解除堵塞,加入到锁竞争中.

wait的使用

1. wait需要在synchronized内部使用,不然会抛出异常.

2. wait的对象需要和synchronized的对象时一致的.

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

 

这时我们可以打开jcons观察:

 

通过这里我们观察到main线程在wait这里进入了堵塞状态.需要解除堵塞我们就需要使用notify方法. 

notify方法

notify方法就是用来解除wait造成的堵.

notify是不用在synchronized中使用的.比如在操作系统中也有wait和notify,notify是不用先加锁再使用的.但在Java中notify需要在synchronized中使用,不然会报错.

public class TreadDemo2 {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) {throw new RuntimeException(e);}System.out.println("wait方法后");}});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (object) {System.out.println("notify方法前");object.notify();System.out.println("notify方法后");}});t1.start();t2.start();}
}

 

代码执行过程:

1) t1和t2启动,并发执行.

2) t1拿到锁会进入阻塞等待

3) t2会先休眠一秒,这会让t1先拿到锁

4) 等到t2休眠结束, t1已经进入wait,将锁解除了,这时t2就可以拿到锁

5) 等到t2执行到notify时,t1的堵塞等待结束重新进入到锁竞争中.

6) 虽然t1等待结束但是t2还没释放锁,再等待t2释放锁后,t1才能拿到锁继续执行. 

注意

1. wait方法有三个可以使用:

第一个是死等,这个对代码非常的不利,一但忘记使用notify,线程就会卡死.我们一般常用的是第二种,有时间限制的等待,超过这个时间就不等了.第三个是微秒级的等待.

2. wait和notify的所对象得是一致的,不然会导致wait的堵塞等待解除不了.

3. notifyAll方法可以解除在多个线程使用同一个锁的wait的堵塞,但是这样不利于代码控制,我们还是比较推荐使用notify.

wait和sleep的区别

相同点:

1. sleep是指定时间的,wait也有指定时间的版本.

2. sleep和wait都可以提前唤醒. sleep是interrupt方法,wait是notify方法.

不同点:

1. sleep是在知道要休眠多久的情况下使用,wait是在不知道要等待多久的情况下使用

2. wait需要在synchronized中使用,sleep不需要.

3. wait是Object方法. sleep是Thread的静态方法.

Java标准库中的线程安全类

线程不安全类

ArrayList

LinkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder

线程安全类

Vector

HashTable

ConcurrentHashMap

StringBuffer

String

这里前四个为线程安全主要是加了synchronizednized关键字不过这几个类jdk都快弃用了.

String为线程安全是因为它不可改变,就不涉及到线程安全问题了.

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

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

相关文章

配电房综合监控系统

配电房综合监控系统是一种集成了实时监控、数据采集、远程控制等多功能的系统&#xff0c;用于对配电房进行全方位的监测和管理。 力安科技配电室综合监控系统依托电易云-智慧电力物联网&#xff0c;实现配电室环境监测、有害气体监测、安防监控、采暖通风、门禁、灯光、风机、…

进阶C语言-字符函数和字符串函数

字符函数和字符串函数 &#x1f388;1.函数介绍&#x1f50e;1.1strlen函数&#x1f52d;1.1.1strlen函数的模拟实现&#x1f4d6;1.计数器法&#x1f4d6;2.递归法&#x1f4d6;3.指针-指针 &#x1f50e;1.2strcpy函数&#x1f52d;1.2.1strcpy函数的模拟实现 &#x1f50e;1…

Linux系统编程 day07 信号

Linux系统编程 day07 信号 1. 信号的介绍以及信号的机制2. 信号相关函数2.1 signal2.2 kill2.3 abort和raise2.4 alarm2.5 setitimer 3. 信号集4. 信号捕捉函数6. SIGCHLD信号7. SIGUSR1与SIGUSR2 1. 信号的介绍以及信号的机制 信号是信息的载体&#xff0c;在Linux/Unix环境下…

如何有效的进行 E2E

一、前言 本文作者介绍了什么是E2E测试以及E2E测试测什么&#xff0c;并从对于被测系统、测试用例、测试自动化工具、测试者四个方面的要求&#xff0c;介绍了如何保证E2E测试有效性&#xff0c;干货满满&#xff0c;值得学习。 二、什么是E2E测试 相信每一个对自动化测试感…

需求不明确的情况下,测试该如何处理?

当需求不明确的情况下&#xff0c;测试团队可以采取以下措施来处理&#xff1a; 1. 与项目团队进行沟通&#xff1a;测试团队应与项目团队密切合作&#xff0c;与业务分析师、产品经理等相关人员进行沟通&#xff0c;以获取更多的需求细节和背景信息。通过与相关方的交流&…

zookeeper 客户端常用命令简单记录(实操课程系列--watcher功能测试)(发布订阅功能测试)

本系列是zookeeper相关的实操课程&#xff0c;课程测试环环相扣&#xff0c;请按照顺序阅读测试来学习zookeeper。阅读本文之前&#xff0c;请先阅读----zookeeper 单机伪集群搭建简单记录&#xff08;实操课程系列&#xff09; 1、命令行工具切换到zookeeper的bin目录下面&am…

springboot自定义校验注解的实现

自定义校验注解的实现 通过谷粒商城项目学习了自定义校验器的实现一、编写自定义校验注解二、自定义注解的校验器三、关联自定义的校验器和自定义的校验注解总结 通过谷粒商城项目学习了自定义校验器的实现 近日在学习雷神的谷粒商城项目&#xff0c;其中有一个自定义校验的实…

国外客户跟我要佣金,该给不该给?

“Jack&#xff0c;这次你要是不帮我&#xff0c;我就死定了&#xff01;” 收到美国公司采购Antony的信息时&#xff0c;我有些哭笑不得&#xff0c;因为在我电脑屏幕上除了他的信息外&#xff0c;还有来自他公司监察部门的邮件&#xff1a; “jack先生&#xff0c;我们调查…

二极管钳位电路的作用

1、使用钳位二极管的瞬态保护即将输出电压限定在某个区间 钳位二极管不仅仅是为了改变电压基线。它们在缓解瞬态事件方面非常有用&#xff0c;尤其是ESD和雷电浪涌。例如&#xff0c;当输入电压高于 Vh 时&#xff0c;D1 正向偏置。因此&#xff0c;过多的电流流过 D1 而不是负…

Linux Centos系统安装Mysql8.0详解

本文是基于服务器Linux Centos 8.0系统 安装 Mysql8.0真实运维工作实战为例&#xff0c;详细讲解安装的全过程。 1&#xff0c;检查卸载mariadb Mariadb数据库是mysql的分支&#xff0c;mariadb和mysql会有冲突&#xff0c;所以安装Mysql前&#xff0c;首先要检查是否安装了m…

java学习part23异常try catch

124-异常处理-异常的概述与常见异常的举例_哔哩哔哩_bilibili 1.异常 2.try catch 3.finally 类似golang的defer 一定执行的语句

7-1 哈夫曼树与哈夫曼编码

哈夫曼树与哈夫曼编码 题目描述输入格式输出格式输入样例输出样例 分数 30 作者 伍建全 单位 重庆科技学院 题目描述 哈夫曼树(Huffman Tree)又称最优二叉树&#xff0c;是一种带权路径长度最短的二叉树。所谓树的带权路径长度&#xff0c;就是树中所有的叶结点的权值乘上其到…

为什么要在项目中使用TypeScript?

随着越来越多的开发人员采用TypeScript&#xff0c;人们需要了解在下一个项目中应该使用TypeScript的原因。尽管它在早期应用中遇到了一些阻力&#xff0c;但在过去十年&#xff0c;它迅速成为一种广泛使用的编程语言。 以下介绍如何使用TypeScript以及它给开发人员带来的一些好…

堆在排序中的应用

堆排序 1、堆排序原理 堆排序是利用到了堆这种数据结构&#xff0c;我们首先回顾一下二叉堆的特性&#xff1a; 最大堆的堆顶是整个堆中的最大元素。最小堆的堆顶是整个堆中的最小元素。 以最大堆为例&#xff0c;如果删除一个最大堆的堆顶&#xff08;并不是完全删除&…

如何利用树莓派与Nginx结合内网穿透服务实现远程访问内部站点——“cpolar内网穿透”

文章目录 1. Nginx安装2. 安装cpolar3.配置域名访问Nginx4. 固定域名访问5. 配置静态站点 安装 Nginx&#xff08;发音为“engine-x”&#xff09;可以将您的树莓派变成一个强大的 Web 服务器&#xff0c;可以用于托管网站或 Web 应用程序。相比其他 Web 服务器&#xff0c;Ngi…

Electron+Ts+Vue+Vite桌面应用系列:TypeScript常用语法详解

文章目录 1️⃣ TypeScript常用讲解1.1 使用1.2 字符串1.3 数字1.3 布尔1.4 数组1.5 元组1.6 枚举1.7 any1.8 void1.9 object1.10 函数指定返回值的类型1.11 联合类型1.12 类型断言1.13 接口1.14 函数类型1.15 类类型1.16 泛型 2️⃣ 类2.1 类的基本写法2.2 类的继承2.3 类的修…

数据结构 | 二叉树的概念及前中后序遍历

数据结构 | 二叉树的概念及前中后序遍历 文章目录 数据结构 | 二叉树的概念及前中后序遍历一、树概念及结构1.1 树的相关概念 二、树的表示2.2 树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; 三、二叉树概念及结构3.1 二叉树的基本概念3.2 二叉树的结构&a…

文档理解的新时代:LayOutLM模型的全方位解读

一、引言 在现代文档处理和信息提取领域&#xff0c;机器学习模型的作用日益凸显。特别是在自然语言处理&#xff08;NLP&#xff09;技术快速发展的背景下&#xff0c;如何让机器更加精准地理解和处理复杂文档成为了一个挑战。文档不仅包含文本信息&#xff0c;还包括布局、图…

熟练运用这些黑盒测试知识点,月薪翻倍不是难题

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

NX二次开发UF_MTX2_copy 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_MTX2_copy Defined in: uf_mtx.h void UF_MTX2_copy(const double mtx_src [ 4 ] , double mtx_dst [ 4 ] ) overview 概述 Copies the 2x2 matrix elements from the source m…