线程知识总结(一)

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 任务:

在这里插入图片描述

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

示例如下:

// 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 后交给 Thread
        FutureTask<String> futureTask = new FutureTask<>(this);
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

	// 任务是读取文件内容,并发它转换成字符串作为返回值
    @Override
    public 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 的构造方法:

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 赋值给成员变量的 target
        init(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:

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new Runnable() {
            @Override
            public 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 interrupted
	at 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() 基本上是我们推荐的,唯一的中断线程的方法。或许有人会问,自己在线程中定义一个中断标记位不是也能实现线程中断嘛,像这样:

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() 这样的阻塞方法,那么该线程就会进入等待状态,在线程池中等待唤醒,在没有其它线程唤醒它的情况下,它就无法通过标记位的方式结束线程:

	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() 为例,进入源码看下中断异常是如何被抛出的:

    /**
     * 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() 的线程执行完后,才恢复可执行状态,与其它线程争夺执行权。比如说:

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() 释放执行权,不释放锁。
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() 输出线程信息,线程的字符串表现形式,包括线程名称、优先级和线程组:

    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 上。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【优选算法】Prefix-Kage:前缀和的算法影(上)

文章目录 1.概念解析2.代码实现2.1【模版】前缀和&#xff08;一维&#xff09;2.1.1 原理2.1.2 代码实现 2.2【模版】前缀和&#xff08;二维&#xff09;2.2.1 原理2.2.2 代码实现 希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力&#xff01; 本篇是优选算…

CVE-2024-32709 WordPress —— Recall 插件存在 SQL 注入漏洞

漏洞描述 WordPress 是一款免费开源的内容管理系统,适用于各类网站,包括个人博客、电子商务系统、企业网站。其插件 WP-Recall 的 account 存在 SQL 注入漏洞,攻击者可以通过该漏洞获取数据库敏感信息。 WP-Recall 版本 <= 16.26.5 漏洞复现 搭建环境、安装插件、完成…

网络安全概论——虚拟专网VPN技术

一、VPN概述 1、VPN的概念 所谓虚拟专网&#xff08;Virtual Private Network VPN&#xff09;是指将物理上分布在不同地点的网络通过公用网络连接而构成逻辑上的虚拟子网&#xff0c;它采用认证、访问控制、机密性、数据完整性等安全机制在公用网络上构建专用网络。 如何理…

mobilenetv2-inceptionv3-resnet50三大模型对比实现人脸识别反欺诈系统【带UI界面】

完整项目包获取→点击文章末尾名片&#xff01; 关于数据集&#xff1a;超大规模人脸欺诈数据集。共70多G。 关于模型对比&#xff1a; inceptionv3&#xff1a; mobilenetv2&#xff1a; resnet50&#xff1a; 关于系统&#xff1a; 界面&#xff1a;

十一、e2studio VS STM32CubeIDE之宏函数展开

目录 一、概述/目的 二、复杂宏函数举例 三、编译-预处理 四、stm32cubeide和e2studio的预处理 五、source insight和vscode 一、概述/目的 复杂宏函数如何快速展开 二、复杂宏函数举例 #define R_BSP_MODULE_START(ip, channel) {FSP_CRITICAL_SECTION_DEFI…

FreeRTOS的任务调度

1.启动任务调度器 vTaskStartScheduler void vTaskStartScheduler( void ) { BaseType_t xReturn;/* Add the idle task at the lowest priority. */#if ( INCLUDE_xTaskGetIdleTaskHandle 1 ){/* Create the idle task, storing its handle in xIdleTaskHandle so it canbe …

【Java基础面试题024】Java中包装类型和基本类型的区别是什么?

回答重点 基本类型&#xff1a; Java中有8种基本数据类型&#xff08;byte、short、int、long、float、double、char、boolean&#xff09;他们是直接存储数值的变量&#xff0c;位于栈上&#xff08;局部变量在栈上、成员变量在堆上&#xff0c;静态字段/类在方法区&#xf…

SpringBoot3+Vue3开发在线考试系统

项目介绍 项目分为3种角色&#xff0c;分别为&#xff1a;超级管理员、老师、学生。超级管理员&#xff0c;负责系统的设置、角色的创建、菜单的管理、老师的管理等功能&#xff0c;也可以叫做系统管理员&#xff1b;老师角色&#xff0c;负责系统业务的管理&#xff0c;包括学…

第3节 测试套件数据驱动

创建Excel、 CSV测试数据 1. 从主菜单中选择 File > New > Test Data。将显示新的测试数据对话框。输入测试数据的名称并选择数据类型作为Excel File/ CSV File 。单击OK。 2. 浏览到要导入Katalon Studio的Excel File, 选择Excel中的sheetName&#xff0c;或者CSV文件…

跨站点请求伪造(Cross Sites Request Forgery)类漏洞攻击方式与防御措施|软件安全测试技术系列

本系列文章分享JavaScript语言常见的安全漏洞&#xff0c;漏洞的原理&#xff0c;可能导致的安全问题&#xff0c;以及如何防御与避免。本文分享的是跨站点请求伪造&#xff08;Cross Sites Request Forgery&#xff09;。 跨站点请求伪造&#xff0c;指利用用户身份操作用户账…

【图像分类实用脚本】数据可视化以及高数量类别截断

图像分类时&#xff0c;如果某个类别或者某些类别的数量远大于其他类别的话&#xff0c;模型在计算的时候&#xff0c;更倾向于拟合数量更多的类别&#xff1b;因此&#xff0c;观察类别数量以及对数据量多的类别进行截断是很有必要的。 1.准备数据 数据的格式为图像分类数据集…

飞牛os使用ddns-go配合华为云实现内网穿透

DDNS-Go 是一个开源的动态域名解析工具&#xff0c;它支持多种操作系统&#xff0c;包括 Windows、Mac 和 Linux&#xff0c;并且支持 ARM 和 x86 架构。以下是使用 DDNS-Go 的基本步骤&#xff1a; 1. 下载和安装&#xff1a; 访问 DDNS-Go 的 GitHub 仓库&#xff08;&#x…

易语言OCR证件照文字识别

一.引言 文字识别&#xff0c;也称为光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;是一种将不同形式的文档&#xff08;如扫描的纸质文档、PDF文件或数字相机拍摄的图片&#xff09;中的文字转换成可编辑和可搜索的数据的技术。随着技…

二八(vue2-04)、scoped、data函数、父子通信、props校验、非父子通信(EventBus、provideinject)、v-model进阶

1. 组件的三大组成部分(结构/样式/逻辑) 1.1 scoped 样式冲突 App.vue <template><!-- template 只能有一个根元素 --><div id"app"><BaseOne></BaseOne><BaseTwo></BaseTwo></div> </template><script…

3D工具显微镜的测量范围

一、测量尺寸范围 样品尺寸&#xff1a; 3D工具显微镜通常能够测量各种尺寸和形状的样品&#xff0c;从小至微米级别的微小结构到大至几厘米甚至更大的物体。具体的测量尺寸范围取决于显微镜的载物台大小、镜头焦距以及软件处理能力。测量精度&#xff1a; 3D工具显微镜的测量…

C#—扩展方法

扩展方法 扩展方法是C#中一种特殊的静态方法&#xff0c;它定义在一个静态类中&#xff0c;但是可以像实例方法一样被调用&#xff0c;使得代码看起来更为直观和易于阅读。扩展方法允许你在不修改原始类的情况下&#xff0c;添加新的方法到现有的类型中。 有↓箭头的是扩展方…

vertx idea快速使用

目录 1.官网下载项目 2.修改代码 2.1拷贝代码方式 为了能够快速使用&#xff0c;我另外创建一个新的maven项目&#xff0c;将下载项目的src文件和pom文件拷贝到新建的maven项目。 2.2删除.mvn方式 3.更新配置 4.配置application 5.idea启动项目 1.官网下载项目 从vert…

分布式全文检索引擎ElasticSearch-数据的写入存储底层原理

一、数据写入的核心流程 当向 ES 索引写入数据时&#xff0c;整体流程如下&#xff1a; 1、客户端发送写入请求 客户端向 ES 集群的任意节点&#xff08;称为协调节点&#xff0c;Coordinating Node&#xff09;发送一个写入请求&#xff0c;比如 index&#xff08;插入或更…

android EditText密码自动填充适配

android上的密码&#xff08;其实不仅仅是密码&#xff0c;可以是用户名也可以是邮箱&#xff09;自动填充&#xff0c;是需要考虑适配的。 官方文档&#xff1a;https://developer.android.com/identity/autofill/autofill-optimize?hlzh-cn 什么是自动填充 手机厂商一般会…

【MySQL】非聚簇索引和聚簇索引,索引的创建、查询、删除

目录 存储引擎是MyISAM 非聚簇索引 主键索引&#xff1a; 普通(辅助)索引&#xff1a; 存储引擎是InnoDB 聚簇索引 主键索引&#xff1a; 普通(辅助)索引&#xff1a; 回表查询 创建索引 创建主键索引 主键索引的特点&#xff1a; 创建唯一索引 唯一索引的特点&am…