线程知识总结(一)

server/2024/12/21 9:37:38/

1、概述

1.1 进程与线程

进程是程序运行时,操作系统进行资源分配的最小单位,包括 CPU、内存空间、磁盘 IO 等。从另一个角度讲,进程是程序在设备(计算机、手机等)上的一次执行活动,或者说是正在运行中的程序。

一个程序进入内存运行时,它就变成一个进程。进程是处于运行中的程序,它拥有自己独立的资源和地址空间,在没有经过进程本身允许的情况下,进程不可以直接访问其他进程的地址空间。同时多个进程可以在单个处理器上并发执行,多个进程之间互不影响。

线程是进程的一个实体,是 CPU 调度的最小单位。自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

使用多线程解决了多任务同时运行的问题,并且,共享变量使得线程间的通信要比进程间通信更有效、更容易。此外,在一些操作系统中,与进程相比,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多。

当然,线程太多,来回切换也会导致执行效率降低。

其实应用程序的执行都是 CPU 在做着快速的切换完成的,这个切换是随机的。在某一个时刻,CPU 只在执行一个线程,但是由于它的执行速度非常快,在毫秒级别,因此人无法感知到它在一个时间片中其实只在执行一个任务,在时间片结束后又去切换执行另一个任务。

1.2 并行与并发

并发是指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果;而并行是指在同一时刻,有多条指令在多个处理器上同时执行。

1.3 JVM 中的多线程解析

JVM 启动时就启动了多个线程,至少有两个线程可以分析的出来:

  1. 执行 main 方法的线程,即主线程。该线程的任务代码都定义在 main 方法中。主线程执行完任务,其所在进程也就关闭了。
  2. 负责垃圾回收的线程。系统会自己决定何时回收垃圾,你也可以通过 System.gc() 通知垃圾回收器来回收。但是这个也不是立即回收垃圾,也是在调用之后的一定时间内。另外 Object 中还有一个 finalize() 方法进行垃圾回收。

实际上,JVM 启动的完整线程有以下这些:

  1. main:main线程,用户程序入口
  2. Reference Handler:清除Reference的线程
  3. Finalizer:调用对象finalize方法的线程
  4. Signal Dispatcher:分发处理发送给JVM信号的线程
  5. Attach Listener:内存 dump,线程 dump,类信息统计,获取系统属性等
  6. Monitor Ctrl-Break:监控 Ctrl-Break 中断信号的

此外,多线程运行时的示意图如下:

请添加图片描述

说明:

  1. 当前有3个线程:main 线程、Thread-1、Thread-2,它们每个都维护了自己的方法栈(run 方法在栈底)。在哪个线程调用了方法,这个方法就会进入哪个线程。
  2. 如果 main 线程先于另外两个线程执行完,JVM 不会结束,而是等所有线程都运行完。
  3. 在3个线程都运行的前提下,如果在某个线程中发生了异常导致该线程停止,不会影响其它线程,其它线程该怎么执行就怎么执行

2、使用

2.1 创建线程

创建线程的目的是为了开启一条执行路径,去运行指定的代码(即该线程的任务)和其他代码实现同时运行。

创建线程的方法有两种:继承 Thread 类和实现 Runnable 接口,实现 Callable 接口严格讲是属于第二种方式,不能单独作为一种方法。

我们先来了解下 Callable 的基本用法再解释上述观点的原因。

Callable 的用法

Callable 是一个函数式接口,有泛型限制,该泛型参数类型与作为线程执行体的 call() 返回值类型相同。call() 比 Runnable 的 run() 功能要更强大,因为它可以有返回值,并且可以声明抛出异常

虽然 call() 也是线程执行体,但是由于 Callable 接口不是 Runnable 的子接口,所以 Callable 并不能直接作为 Thread 的 target,而是要借助 Future 接口。

Future 接口代表 call() 的返回值,而且 Future 的实现类 FutureTask 也实现了 Runnable 接口,可以作为 Thread 的 target。Future 中定义了如下的公共方法控制与它关联的 Callable 任务:

在这里插入图片描述

创建并启动有返回值的线程的步骤:

示例如下:

java">// Callable 后的参数类型为 String,意味着 call() 的返回值类型为 String
public class CallableTest implements Callable<String> {public static void main(String[] args) {new CallableTest().test();}/*** Callable 的 call() 类似于 Runnable 的 run(),只不过前者有返回值而前者没有。** 此外,Future 接口可以控制 Runnable/Callable 取消执行任务、查询任务是否完成、* 获取任务执行结果(通过阻塞方法 get 获取结果)。** 由于 Future 接口不能直接实例化,所以一般都是使用 FutureTask,它实现了 RunnableFuture* 接口,RunnableFuture 又继承了 Runnable 和 Future,所以它既可以作为 Runnable 被线程执行,* 又可以作为Future得到Callable的返回值。*/private void test() {// 将 Callable 包装进 FutureTask 后交给 ThreadFutureTask<String> futureTask = new FutureTask<>(this);new Thread(futureTask).start();try {System.out.println(futureTask.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}// 任务是读取文件内容,并发它转换成字符串作为返回值@Overridepublic String call() throws Exception {StringBuffer stringBuffer = new StringBuffer();try (FileInputStream fileInputStream = new FileInputStream("filepath");FileChannel inChannel = fileInputStream.getChannel()) {ByteBuffer byteBuffer = ByteBuffer.allocate(256);while (inChannel.read(byteBuffer) != -1) {byteBuffer.flip();Charset charset = Charset.forName("GBK");CharBuffer charBuffer = charset.decode(byteBuffer);stringBuffer.append(charBuffer);byteBuffer.clear();}}return stringBuffer.toString();}
}

原因解释

我们结合 Thread 的源码解释下原因。首先,Thread 的构造方法中并没有 Callable 作为参数的:

只有空参和接收 Runnable 的构造方法:

java">public class Thread implements Runnable {/* What will be run. */private Runnable target;public Thread() {// nextThreadNum() 初始为 0,可以看到线程编号在创建线程时就已经确定init(null, null, "Thread-" + nextThreadNum(), 0);}public Thread(Runnable target) {// init() 会将参数 target 赋值给成员变量的 targetinit(null, target, "Thread-" + nextThreadNum(), 0);}public void run() {if (target != null) {target.run();}}
}

关注运行任务的 run():

  • 如果使用继承 Thread 的方式创建线程,那么 run() 被重写,执行任务时就按照 Thread 子类的 run() 去执行。
  • 如果使用实现 Runnable 的方式创建线程,run() 在判断 target 不为空之后会运行 target 的 run()。可以认为 Runnable 就是对线程的任务进行了对象的封装。

因为 FutureTask -> RunnableFuture -> Runnable & Future,而 Callable 需要交给 FutureTask 才能执行,所以实现 Callable 接口这种创建线程的方式在实现 Runnable 接口的范畴内,不能作为一种单独的创建方式。

两种创建线程的方式对比

两种方式各有优缺点,通常还是使用实现 Runnable 接口的方式:

  • 继承 Thread 类方式编写简单如果要访问当前线程无须使用 Thread.currentThread(),直接使用 this 即可。劣势是不能再继承其它父类。
  • 实现 Runnable 接口方式则可以继承其它类,并且多个线程可以共享同一个 target 对象,非常适合多个相同线程来处理同一份资源,从而将 CPU、代码和数据分开,形成清晰的模型。缺点就是编程稍复杂,必须使用 Thread.currentThread() 访问当前线程。

2.2 停止线程

线程会因为如下两个原因之一被停止:

  1. run() 正常退出而自然死亡
  2. 因为一个没有捕获的的遗产终止了 run() 而意外死亡

推荐使用 Thread 的 interrupt() 配合 isInterrupted()/interrupted() 来停止线程。

interrupt()

interrupt() 会请求线程停止,注意是请求,而不是立即停止线程。该方法会将线程中的中断状态标记位置位,等待线程通过 isInterrupted()/interrupted() 检查该标记位来进行响应。isInterrupted() 和 interrupted() 都会在标记位被置位的情况下返回 true,不同点在于前者是对象方法,而后者是一个静态方法,并且在调用后会将标记位改写为 false。

在使用上述方法时需要注意,如果在中断标记位为 true 的情况下执行阻塞方法(如 Thread.sleep()、Thread.join()、Object.wait()),这些阻塞方法会抛出 InterruptedException,并且在抛出异常后立即将线程的中断标示位清除重置为 false:

java">    public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {System.out.println("Thread is running...");try {Thread.sleep(500);} catch (InterruptedException e) {System.out.println(Thread.currentThread().isInterrupted());e.printStackTrace();// Thread.currentThread().interrupt();}}}});thread.start();Thread.sleep(1000);thread.interrupt();System.out.println("interrupt in main!");}

如果不打开 catch 中被注释掉的 Thread.currentThread().interrupt(),线程是无法被中断的,因为 sleep() 如果发现中断标记位为 true 会抛出异常并将其清除为 false:

Thread is running...
Thread is running...
Thread is running...
Thread is running...
interrupt in main!
false
Thread is running...
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.demo.thread.multi.InterruptDemo$1.run(InterruptDemo.java:13)at java.lang.Thread.run(Thread.java:748)
Thread is running...
Thread is running...
// 线程继续运行输出 Thread is running...

从这里也能看到子线程其实是在主线程结束后才消亡的,一定注意要让线程能执行完,否则这个线程会阻止已经运行完的主线程所在的进程结束

在抛出 InterruptedException 的 catch 代码块中调用 interrupt() 中断线程是基本操作。

自定义标记位

interrupt() 基本上是我们推荐的,唯一的中断线程的方法。或许有人会问,自己在线程中定义一个中断标记位不是也能实现线程中断嘛,像这样:

java">class StopThread implements Runnable {private boolean flag = true;public void run() {while (flag) {System.out.println(Thread.currentThread().getName() + "......++++");}}// 外部调用注入方法控制标记位进而停止线程public void setFlag(boolean flag) {this.flag = flag;}
}

一般情况下确实可以,但如果线程中执行的代码有类似 wait() 这样的阻塞方法,那么该线程就会进入等待状态,在线程池中等待唤醒,在没有其它线程唤醒它的情况下,它就无法通过标记位的方式结束线程:

java">	public synchronized void run() {while (flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "......++++");}}

在这种情况下,使用 interrupt() 会更好,因为:

  1. 一般的阻塞方法,如 sleep()、wait() 等本身就支持中断的检查
  2. 检查中断标记位和自定义的标志位没什么区别,用中断标记位还可以避免声明自定义的标志位,减少资源的消耗
  3. interrupt() 方法会将线程从阻塞状态强制恢复到运行状态中来,让线程具备 cpu 的执行资格,但是强制动作会发生 InterruptedException,需要处理

中断异常是如何被抛出的

我们以 Thread.sleep() 为例,进入源码看下中断异常是如何被抛出的:

java">    /*** sleep 期间,线程不会失去已经获取到的同步锁。** @throws  InterruptedException*          如果任何线程中断了当前线程,那么会抛出这个异常并且*          清除掉当前线程的中断状态。*/public static native void sleep(long millis) throws InterruptedException;

想要查看 sleep() 的 native 源码,要先在 src/share/native/java/lang/Thread.c 文件中,找到 sleep() 在 JVM 中对应的方法 JVM_Sleep:

#include "jni.h"
#include "jvm.h"#include "java_lang_Thread.h"#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))static JNINativeMethod methods[] = {{"start0",           "()V",        (void *)&JVM_StartThread},{"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},{"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},{"suspend0",         "()V",        (void *)&JVM_SuspendThread},{"resume0",          "()V",        (void *)&JVM_ResumeThread},{"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},{"yield",            "()V",        (void *)&JVM_Yield},{"sleep",            "(J)V",       (void *)&JVM_Sleep},{"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},{"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},{"interrupt0",       "()V",        (void *)&JVM_Interrupt},{"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},{"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},{"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},{"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},{"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};#undef THD
#undef OBJ
#undef STE
#undef STR// 注册 methods[] 中的 native 方法
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

然后去 jvm.cpp 文件中找到这个方法:

可以看到如果线程的中断标记位已经为 true,调用 sleep 方法就会抛出 InterruptedException。在抛出 InterruptedException 之前,中断标记位会被清除为 false。

3、线程的状态

3.1 状态定义与状态转移

线程状态也被称为生命周期,指的是 JVM 中的线程状态,而不是操作系统的。Thread 中定义的枚举类 State 规定了线程的 6 种状态:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用 start()。这时仅仅由虚拟机分配内存并初始化变量值。如果此时错误地调用了 run(),该线程就不再处于初始状态,不能再调用 start()。
  2. 可运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态统称为“可运行”。
    线程对象创建后,其他线程(如主线程)调用了该对象的 start() 后会进入就绪状态,虚拟机会创建方法调用栈和程序计数器。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权。即线程可以运行,但是尚未运行。
    就绪状态的线程在获得 CPU 时间片后,开始执行 run() 就变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(如通知或中断)。
  5. 等待超时(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时间后自行返回,也就是计时等待。
  6. 终止(TERMINATED):表示该线程已经执行完毕,包括 run() 或 call() 执行完成,线程正常结束;线程抛出未捕获的 Exception 或 Error;直接调用了 stop() 结束线程(容易死锁,不推荐)。

状态转移图如下所示:

说明:

  1. 主线是初始->运行->终止,new 创建的新线程要调用 start() 才能进入运行状态。运行状态内部又分为运行中(具备执行权)和就绪(具备执行资格但没有执行权)两种状态。注意 Java 中是把运行中和就绪统一视为运行状态,但是在操作系统的观点中,认为这两个状态是分开的、两个独立的状态
  2. 运行状态的线程可以通过调用 Object.wait() 或 Thread.sleep() 等方法进入等待状态,方法上加时间参数的会进入等待超时状态。等待状态下的线程没有执行资格,需要通过 notify()、notifyAll() 等方法唤醒。
  3. 阻塞状态(具备执行资格但无执行权)只有一种情况,就是等待获取 synchronized 锁,拿到锁之后就变成了运行状态(阻塞式 IO 方法应该是这种情况?)。注意使用显式锁 Lock 等待锁时,进入的是等待/等待超时状态,因为其底层使用的是 LockSupport 类实现的。因此系统中能让线程进入阻塞状态的有且仅有 synchronized 关键字。
  4. 阻塞状态是一种被迫的等待(因为拿不到锁只能等着),而等待状态是一种主动的等待(主动调用 wait() 或 sleep())。

此外还有几点注意事项:

  1. 主线程结束并不会影响其它线程。
  2. isAlive() 可以测试某个线程是否存活,就绪、运行、阻塞状态返回 true,新建、死亡返回 false。
  3. 不要对已经死亡的线程调用 start(),也不要对新建的线程调用两次 start(),否则会引发 ILLegalThreadStateException。

针对以上情况,当发生如下特定情况时可以解除上面的阻塞,使线程重新进入就绪状态:

3.2 涉及的方法介绍

join()

可以通过 join() 控制线程的执行顺序,哪个线程执行到了 join() 就释放执行权并冻结在线程池中,等调用了 join() 的线程执行完后,才恢复可执行状态,与其它线程争夺执行权。比如说:

java">public static void main(String[] args) throws Exception{Demo d = new Demo();Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();t2.start();t1.join();//t1线程要申请加入进来,运行。临时加入一个线程运算时可以使用join方法。for(int x=0; x<50; x++){System.out.println(Thread.currentThread()+"....."+x);}}
}

主线程开启了 t1、t2 两个线程,在执行 t1.join() 之前,是三个线程在轮番运行的。在主线程中执行了 t1.join() 后,主线程释放执行权,冻结在线程池。等待 t1 执行完毕后,恢复可执行状态,与 t2 争夺执行资格,轮番运行。

如果在 A 线程中调用了 B 线程的 join() 方法,那么 A 线程将被阻塞直到 B 线程执行完。它有三种重载形式:

第三种形式很少被用到,因为程序、操作系统和计算机硬件都无法精确到纳秒。

sleep()

sleep() 用于线程睡眠,调用该方法可以让线程暂停一段时间进入等待状态,它有两种重载形式:

同样是因为程序、操作系统和硬件设备无法精确到纳秒,因此第二个方法很少被使用。

处于睡眠时间内的线程不会获得执行的机会,即使系统中没有其它可执行的线程,处于 sleep() 中的线程也不会执行;已经获得锁的线程如果执行了 sleep(),只会释放执行权,但不会释放锁。

yield()

yield() 用于线程让步,它也可以让当前正在运行的线程暂停,但它不会使线程进入等待状态,只是将该线程转入可运行状态,然后让系统的线程调度器重新调度一次。完全可能的情况是:某个线程调用了 yield() 暂停之后,调度器又将其调度出来重新执行。

实际上,当 A 线程调用了 yield() 之后,只有优先级大于等于 A 的处于可运行状态的线程才会获得执行机会。

暂停当前正在执行的线程对象,释放执行权,然后让包括自己在内的所有线程再次争夺执行权。这样做会更和谐,不会一直在执行同一个线程。

sleep() 与 yield() 的区别:

其它方法

Object.wait() 与 Thread.sleep() 的对比:

  1. wait() 可以指定时间也可以不指定,sleep() 必须指定时间。
  2. 在同步中时,对 cpu 的执行权和锁的处理不同。wait() 释放执行权,释放锁;sleep() 释放执行权,不释放锁。
java">class Demo {void show() {synchronized(this) {wait();//t0 t1 t2}}void method() {synchronized(this)//t4{notifyAll();}//t4}
}

同步中谁有锁谁执行,如果 t0 t1 t2 都卡在 wait() 并被 t4 唤醒,虽然 3 个都活了,但是只有一个能持锁执行代码。

此外,我们经常会调用 Thread 的 toString() 输出线程信息,线程的字符串表现形式,包括线程名称、优先级和线程组:

java">    public String toString() {ThreadGroup group = getThreadGroup();if (group != null) {return "Thread[" + getName() + "," + getPriority() + "," +group.getName() + "]";} else {return "Thread[" + getName() + "," + getPriority() + "," +"" + "]";}}

4、线程属性

4.1 优先级

每个 Java 线程都有一个优先级,可以通过 Thread.setPriority() 将线程优先级设置在 MIN_PRIORITY(数值为1)和 MAX_PRIORITY(数值为10)之间,默认优先级为 MIN_PRIORITY(数值为5)。

线程调度器会优先选择优先级高的线程来运行。但是线程优先级是高度依赖于系统的。当 JVM 依赖于宿主机平台的线程实现机制时,Java 线程的优先级会先被映射到宿主机平台的优先级上。例如 Windows 有 7 个优先级,而在 Oracle 为 Linux 提供的 JVM 中,线程的优先级被忽略——所有线程具有相同的优先级。

如果确实要使用优先级,需要注意,如果高优先级的线程没有进入非活动状态(阻塞或等待),低优先级的线程可能永远也得不到执行,发生线程饥饿的情况(线程饥饿就是指低优先级的线程,总是拿不到执行时间)。

4.2 守护线程

守护线程也称为后台线程、精灵线程,它是在后台运行的线程,任务是为其它线程提供服务,JVM 的垃圾回收线程就是典型的守护线程。此外,守护线程也可用于发送计时信号或清空过时的高速缓存。

可以使用 Thread 的 setDeamon(true) 将一个线程设置为守护线程(但是必须在该线程启动之前,否则会引发 ILLegalThreadStateException),isDeamon() 用来判断是否是守护线程。

主线程默认是前台线程,但不是所有线程默认都是前台线程,规则是:前台线程创建的子线程默认是前台线程,守护线程创建的线程默认是后台线程

守护线程也会去争抢同步锁。

守护线程的特征为当所有前台线程死亡后,虚拟机会退出并通知后台线程死亡。假如在主线程中开启了一个执行耗时操作的守护线程,那么很有可能守护线程的任务并不会执行完,因为主线程不会等待守护线程,只要主线程跑完了,守护线程也会自动消亡。但如果在主线程中启动一个非守护线程,那么主线程会等待该子线程执行完任务。

永远不要让守护线程去访问文件、数据库这样的固有资源,因为他会在任何时候甚至在一个操作的中间被中断。

4.3 线程组与未处理的异常

Java 允许程序直接对 ThreadGroup 进行控制。如果没有显式指定一个线程属于哪个线程组,那么它就属于默认线程组,即与创建它的线程在同一线程组。线程中途不能改变它所属的线程组

以下构造方法用来指定新创建的线程属于哪个线程组:

在这里插入图片描述

以上方法也可以指定线程组的名字,这个名字也不能中途更改。

常用的操作线程组的方法:

ThreadGroup 实现了一个接口 Thread.UncaughtExceptionHandler,该接口用于处理未捕获的异常。

线程的 run() 不能抛出任何受查异常(刨去非受查异常剩余的异常),而非受查异常(所有派生于 Error 或 RuntimeException 的异常)会导致线程终止。

线程因为异常终止之前,会将异常传递到未捕获异常的处理器中,该处理器必须实现 Thread.UncaughtExceptionHandler 接口,并且通过 Thread 的 setUncaughtExceptionHandler() 为某一个线程设置处理器,或者用静态的 setDefaultUncaughtExceptionHandler() 为所有线程设置一个默认的处理器。如果没有调用以上方法给线程设置处理器,那么线程的处理器就是该线程的 ThreadGroup 对象。

UncaughtExceptionHandler 接口的唯一方法 uncaughtException() 会按照如下优先顺序操作:

  1. 如果该线程组有父线程组,则调用父线程组的 uncaughtException()
  2. 如果 Thread 的 getDefaultUncaughtExceptionHandler() 返回一个非空处理器,则调用该处理器
  3. 如果 Throwable 是 ThreadDeath 的一个实例,就什么都不做,否则,就将线程名字以及 Throwable 的栈轨迹输出到 System.err 上。

其中,最后一点的栈轨迹就是应用发生崩溃时我们看到的调用栈信息。


http://www.ppmy.cn/server/151914.html

相关文章

Python 爬取网页文字并保存为 txt 文件教程

引言 在网络数据获取的过程中&#xff0c;我们常常需要从网页中提取有用的文字信息。Python 提供了强大的库来帮助我们实现这一目标。本教程将以https://theory.gmw.cn/2023 - 08/31/content_36801268.htm为例&#xff0c;介绍如何使用requests库和BeautifulSoup库爬取网页文字…

微服务设计(第2版)读书笔记

微服务概述 什么是微服务&#xff1f; 答&#xff1a;微服务&#xff08;microservice&#xff09;是基于业务领域建模的&#xff0c;可独立发布的服务。它会把业务内聚的功能封装起来&#xff0c;并通过网络供其他服务访问。将这样的服务组合起来构建出更复杂的系统。 微服务…

Vue.js前端框架教程13:Vue空值合并?? 可选链?.和展开运算符...

文章目录 ??&#xff08;空值合并运算符&#xff09;区别 | - 逻辑或&#xff08;Logical OR&#xff09;|| - 空值合并运算符&#xff08;Nullish Coalescing Operator&#xff09;?. - 可选链&#xff08;Optional Chaining&#xff09;... 展开运算符&#xff08;Spread …

Web应用中的XSS防护实践

什么是XSS攻击&#xff1f; XSS(Cross-Site Scripting)跨站脚本攻击是一种常见的网络安全漏洞。攻击者通过在网页中注入恶意脚本代码,当用户浏览这些页面时,恶意代码会在用户的浏览器中执行,从而盗取用户信息、篡改页面内容或进行其他恶意操作。 XSS攻击的主要类型 1. 反射型…

被裁20240927 --- 嵌入式硬件开发 前篇

前篇主要介绍一些相关的概念&#xff0c;用于常识扫盲&#xff0c;后篇开始上干货&#xff01; 他捧着一只碗吃过百家的饭 处理器芯片处理器芯片制造商嵌入式处理器芯片制造商国内制造商国外制造商 与意法半导体对标的国产芯片制造商一、中芯国际二、华为海思三、紫光国微四、北…

uniapp Native.js 调用安卓arr原生service

最近搞了个uni小项目&#xff0c;一个定制的小平板&#xff0c;带一个nfc设备&#xff0c;厂家只给了一套安卓原生demo&#xff0c;头一次玩原生安卓&#xff0c;废了好半天劲打出来arr包&#xff0c;想镶进uniapp里&#xff0c;网上查了好久&#xff0c;都是错的&#xff0c;要…

113.PyQt5_QtPrintSupport_打印操作

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

关于IP代理API,我应该了解哪些功能特性?以及如何安全有效地使用它来隐藏我的网络位置?

IP代理API是一种服务&#xff0c;允许用户通过访问经过中间服务器的网络连接来改变其公开的互联网协议地址&#xff08;IP&#xff09;&#xff0c;从而达到隐藏真实地理位置的效果。以下是您在选择和使用IP代理API时应关注的一些功能和安全性考虑&#xff1a; 匿名度&#xff…