线程相关知识
主要是过一遍,尽量看懂即可。
线程与进程
进程:程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:一个进程在其执行的过程中可以产生多个线程。
区别:同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多。也被称为轻量级进程。
java 线程与操作系统中的线程的关系:
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。JDK1.2 之前(Green Threads)
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。JDK1.2 及其之后(Native Threads)
- 用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。(现在的 Java 线程的本质其实就是操作系统的线程。)
线程与进程的关系区别及优缺点?
- 一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
拓展内容:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
- 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。(从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。)
如果是
native
方法,则程序计数器记录的是undefined
地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
- 虚拟机栈和本地方法栈是线程私有为了保证线程中的局部变量不被别的线程访问到。
虚拟机栈:(java)方法执行之前创建栈帧存放局部变量表、操作数栈、常量池引用等信息。(调用到完成对应着栈帧的入栈和出栈)
本地方法栈:与虚拟机栈的区别是本地方法栈为虚拟机使用到的 Native 方法服务,而虚拟机栈则是为虚拟机执行 Java 方法 (也就是字节码)服务
- 堆主要用于存放新创建的对象
- 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆和方法区是所有线程共享的资源
创建线程的方式 :
创建线程的方式似乎有很多:线程基础知识(heima)#创建线程的方式
但都属于是在 Java 代码中使用多线程的方法。
大家都说Java有三种创建线程的方式!并发编程中的惊天骗局! (建议读!)
继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池、使用 CompletableFuture 类...
严格来说,Java 就只有一种方式可以创建线程,那就是通过 new Thread().start()
创建。不管是哪种方式,最终还是依赖于 new Thread().start()
。
启动线程的大体过程:
①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)#线程包括哪些状态,状态之间是如何变化的*
- NEW:初始状态,线程被创建出来但没有被调用 start()。
- RUNNABLE:运行状态,线程被调用了 start()等待运行的状态。
- BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕,
对于图像方面需要理解:
在操作系统层面,线程有
READY
和RUNNING
状态;而在 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()
方法对比
都可以暂停线程的执行
sleep()
方法没有释放锁,而wait()
方法释放了锁。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)
超时后线程会自动苏醒,sleep()
是 Thread 类的静态本地方法,wait()
则是 object 类的本地方法。
为什么 wait()
不定义 Thread 类中?
因为 wait()
针对的让获得对象锁的线程等待 ->
释放当前线程的对象锁,每个对象都有对象锁,要释放当前线程的对象锁则是操作对象(Object)而不是线程(Thread)。
为什么 sleep()
定义在 Thread 中?
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
直接运行线程的 run
方法会怎样?
调用
start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()
会执行线程的相应准备工作,然后自动执行run()
方法的内容,这是真正的多线程工作。
直接执行 run()
方法,会把 run()
方法当成一个 main
线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
多线程
并发与并行:
- 并发:两个及两个以上的作业在同一时间段内执行。
- 并行:两个及两个以上的作业在同一时刻执行。
同步与异步:
- 同步:发出一个调用之后,在没有得到结果之前,该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回。
使用多线程的原因
- 线程间的切换和调度的成本远远小于进程。多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
- 对于单核时代:主要是为了提高单进程利用 CPU 和 IO 系统的效率。
假设只运行了一个 Java 进程,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
- 对于多核时代:主要是为了提高进程利用多核 CPU 的能力。
假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 核心上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
Java 使用的线程调度方式
- 抢占式调度(JAVA 使用):操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发,存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。
(Preemptive Scheduling)
- 协同式调度:线程执行完毕后,主动通知系统切换到另一个线程。可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。
(Cooperative Scheduling)
单核 CPU 运行多线程
单核 CPU 支持 Java 多线程,操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程
有两种类型的线程:
- CPU 密集型:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。
- IO 密集型:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
而对于单核 CPU 而言:
- 若为 CPU 密集型:那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。
- 若为 IO 密集型:那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。
多线程带来的问题
有内存泄漏、死锁、线程不安全...
什么是线程安全?
- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
- 线程不安全则可能会导致数据混乱、错误或者丢失。
至于死锁见并发锁相关知识
虚拟线程 :
TODO 优先级低.
JDK21 发布... 虚拟线程常见问题总结 | JavaGuide