线程相关知识

主要是过一遍,尽量看懂即可。

线程与进程

进程:程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

线程:一个进程在其执行的过程中可以产生多个线程。

区别:同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多。也被称为轻量级进程。

java 线程与操作系统中的线程的关系:

线程与进程的关系区别及优缺点?

../../../../ZZZ-Misc/Z-Attachment/images/Pasted image 20241218222742.png|450

Note

拓展内容:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

  • 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。(从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。)

如果是 native 方法,则程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

  • 虚拟机栈和本地方法栈是线程私有为了保证线程中的局部变量不被别的线程访问到

虚拟机栈:(java)方法执行之前创建栈帧存放局部变量表、操作数栈、常量池引用等信息。(调用到完成对应着栈帧的入栈和出栈)
本地方法栈:与虚拟机栈的区别是本地方法栈为虚拟机使用到的 Native 方法服务,而虚拟机栈则是为虚拟机执行 Java 方法 (也就是字节码)服务

  • 堆主要用于存放新创建的对象
  • 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

堆和方法区是所有线程共享的资源

创建线程的方式 :

创建线程的方式似乎有很多:线程基础知识(heima)#创建线程的方式

但都属于是在 Java 代码中使用多线程的方法。

大家都说Java有三种创建线程的方式!并发编程中的惊天骗局! (建议读!)

继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池、使用 CompletableFuture 类...

严格来说,Java 就只有一种方式可以创建线程,那就是通过 new Thread().start() 创建。不管是哪种方式,最终还是依赖于 new Thread().start()

Note

启动线程的大体过程

①Thread 在类加载阶段,就会通过静态代码块去绑定 Thread 类方法与 JVM 本地方法的关系:

private static native void registerNatives();
static {
    registerNatives();
}

执行完这个 registerNatives()本地方法后,Java 的线程方法,就和 JVM 方法绑定了,如 start0()这个方法,会对应着 JVM_StartThread()这个 C++函数等(具体代码位于 openjdk\jdk\src\share\native\java\lang\Thread.c 这个文件)。

②当调用 Thread.start()方法后,会先调用 Java 中定义的 start0(),接着会找到与之绑定的 JVM_StartThread()这个 JVM 函数执行(具体实现位于 openjdk\hotspot\src\share\vm\prims\jvm.cpp 这个文件)。

JVM_StartThread()函数最终会调用 os::create_thread(...)这个函数,这个函数依旧是 JVM 函数,毕竟 Java 要实现跨平台特性,而不同操作系统创建线程的内核函数,也有所差异,如 Linux 操作系统中,创建线程最终会调用到 pthread_create(...)这个内核函数。

④创建出一条内核线程后,接着会去执行 Thread::start(...)函数,接着会去执行 os::start_thread(thread)这个函数,这一步的作用,主要是让 Java 线程,和内核线程产生映射关系,也会在这一步,把 Runnable 线程体,顺势传递给 OS 的内核线程(具体实现位于 openjdk\hotspot\src\share\vm\runtime\Thread.cpp 这个文件)。

⑤当 Java 线程与内核线程产生映射后,接着就会执行载入的线程体(线程任务),也就是 Java 程序员所编写的那个 run()方法。

线程的生命周期与状态

线程基础知识(heima)#线程包括哪些状态,状态之间是如何变化的*

对于图像方面需要理解:
../../../../ZZZ-Misc/Z-Attachment/images/Pasted image 20241214143522.png

../../../../ZZZ-Misc/Z-Attachment/images/Pasted image 20241219161213.png

在操作系统层面,线程有 READYRUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态

为什么不区分这两个状态呢?

现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用 所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。

线程方法

Thread.sleep() 方法和 Object.wait() 方法对比

都可以暂停线程的执行

为什么 wait() 不定义 Thread 类中?

因为 wait() 针对的让获得对象锁的线程等待 -> 释放当前线程的对象锁,每个对象都有对象锁,要释放当前线程的对象锁则是操作对象(Object)而不是线程(Thread)。

为什么 sleep() 定义在 Thread 中?

因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

直接运行线程的 run 方法会怎样?

调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行

多线程

并发与并行:

同步与异步:

使用多线程的原因

Java 使用的线程调度方式

(Preemptive Scheduling)

(Cooperative Scheduling)

单核 CPU 运行多线程

单核 CPU 支持 Java 多线程,操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程

有两种类型的线程:

而对于单核 CPU 而言:

多线程带来的问题

有内存泄漏、死锁、线程不安全...

什么是线程安全?

至于死锁见并发锁相关知识

虚拟线程 :

TODO 优先级低.

JDK21 发布... 虚拟线程常见问题总结 | JavaGuide