线程基础
线程和进程的区别?
线程和进程是操作系统中用于实现并发和并行操作的两个重要概念,它们之间存在一些显著的区别。
一、基本概念
进程(Process)
进程是程序在特定数据集上的一次执行过程,是系统进行资源分配和调度的独立单元。
每个进程都有自己独立的内存空间、系统资源(如文件描述符)和进程控制块(PCB)。
线程(Thread)
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。
线程几乎不拥有系统资源,只拥有一点运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它会共享进程的资源(如内存和文件)。
二、主要区别
资源拥有
- 进程拥有独立的内存空间和系统资源,而线程共享其所属进程的资源。
调度和切换
进程是资源分配的基本单位,它的切换开销较大,因为需要保存和恢复整个进程的上下文。
线程是CPU调度和分派的基本单位,它的切换开销较小,因为线程共享进程的地址空间和其他资源,只需保存和恢复线程的上下文。
并发性
进程可以并发执行,但进程间的通信需要通过显式的机制(如管道、消息队列等)来实现,开销较大。
线程之间的通信更加方便快捷,因为它们共享进程的地址空间和资源,可以直接读写共享数据。
独立性
进程是独立的执行环境,一个进程的崩溃不会影响到其他进程。
线程虽然有自己的执行路径,但线程间的同步和加锁控制相对复杂,一个线程的崩溃可能会影响到整个进程的稳定性。
创建和销毁
进程的创建和销毁相对复杂,需要操作系统的支持,开销较大。
线程的创建和销毁相对简单,可以更灵活地进行管理,开销较小。
三、总结
- 进程:独立的执行环境,拥有独立的资源,切换开销大,但稳定性高,适合进行大型任务和资源管理。
- 线程:进程的一部分,共享进程资源,切换开销小,但同步和加锁控制复杂,适合进行细粒度的并发操作,提高程序的执行效率。
理解线程和进程的区别对于编写高效、安全和可靠的程序至关重要。在实际开发中,应根据具体需求选择合适的并发模型。
Java线程实现模型?
Java的线程实现模型主要涉及到线程在Java虚拟机(JVM)中的表示方式以及它们如何与操作系统中的线程或进程进行交互。
Java线程与操作系统的关系
- 线程是操作系统的最小调度单位:线程是包含在进程中的,是CPU调度的基本单位。Java中的线程模型需要考虑到如何在不同的操作系统平台上实现统一的线程操作。
线程类型:
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。用户线程创建和切换成本低,但不可以利用多核。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。内核态线程,创建和切换成本高,可以利用多核。
线程的3 种主要实现方式
使用内核线程实现:
- 内核线程(KLT):直接由操作系统内核支持的线程。Java线程可以通过操作系统提供的线程接口(如Linux的LWP)来实现,这种线程由内核来完成线程切换和调度,并负责将线程的任务映射到各个处理器上。程序一般不会去直接使用内核线程,而是使用内核线程的轻量级进程(LWP)接口,即通常意义上的线程。由于每个轻量级进程都由一个内核线程支持,轻量级进程与内核线程之间 1:1 关系称为一对一的线程模型。
- 优点:可以充分利用多核处理器的并行处理能力,线程的创建、调度和管理等生命周期由操作系统内核直接管理,编程和实现相对简单。
- 缺点:线程的创建、销毁和切换需要频繁进行用户态和内核态之间的切换,资源消耗较大,效率相对较低。
使用用户线程实现:
- 用户线程(UT):完全建立在用户空间的线程库上,系统内核不能感知线程的存在。用户线程的创建、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
- 优点:由于不依赖内核,线程的操作更加快速且低消耗,可以支持规模更大的线程数量。
- 缺点:需要用户程序自己处理线程的创建、切换和调度等问题,编程和实现复杂。线程阻塞会导致整个进程阻塞,且不能充分利用多核处理器的并行处理能力,一般的应用程序都不倾向使用用户线程。Java、Ruby等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如Golang、Erlang等。
混合实现(用户线程加轻量级进程):
- 混合模型:既存在用户线程,也存在轻量级进程(LWP)。用户线程完全建立在用户空间中,用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,是N:M的关系,许多UNIX系列的操作系统等都提供了M:N的线程模型实现。
- 优点:结合了用户线程和内核线程的优点,既可以支持大规模的用户线程并发,又可以利用内核提供的线程调度功能和处理器映射。
- 缺点:实现复杂度较高,需要处理用户线程和轻量级进程之间的转换和调度。
Java线程的实现模型
Sun JDK的实现:
Windows和Linux版:通常使用一对一的线程模型实现,即一个Java线程映射到一个轻量级进程(LWP)中。
Solaris版:可以同时支持一对一和多对多的线程模型,通过特定的虚拟机参数来选择使用哪种模型。即
**-XX:+UseLWPSynchronization**
(默认值)和-XX:+UseBoundThreads
。线程调度:
Java使用抢占式线程调度方式,线程的执行时间由系统分配和控制。
Java线程实现模型主要涉及到使用内核线程、用户线程或混合实现的方式。不同的实现方式有不同的优缺点,Java通过提供统一的线程API来屏蔽这些差异,使得开发者可以在不同的操作系统平台上编写可移植的并发程序。在实际应用中,开发者可以根据具体的需求和场景选择合适的线程实现方式。
Java 线程线程调度策略?
线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种:
- 抢占式调度:抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
- 协同式调度: 协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:线程执行时间不可控制,如果一个线程编写有问题,导致线程堵塞,那么可能导致整个系统崩溃。
- 混合调度:混合了抢占式和协作式两种策略的调度方式
JAVA中线程调度策略
Java中的线程调度主要取决于底层的操作系统,因为Java运行在JVM(Java虚拟机)之上,JVM运行在操作系统之上。Java语言规范并未定义一个特定的线程调度算法。因此,Java的线程调度可以在不同的JVM和操作系统实现中有所不同。
在Java中有两种基础的线程调度方式:分时调度和优先级调度。
- 分时调度(Time Scheduling):在这种方式下,所有线程都获得等量的CPU时间来执行代码。一旦一个线程的时间片用完,操作系统会将CPU切换到另一个线程。当操作系统提供此类调度策略并且JVM采用该策略时,确实存在时间片分配机制。对于每个线程,都会均等地分配处理器时间,以尽可能公平地处理各个线程。
- 优先级调度(Priority Scheduling):在这种方式下,每个Java线程都有一个优先级,范围在1到10。在给定时间,CPU将优先执行优先级高的线程。当优先级相同的线程存在时,采取分时调度。
由于具体的线程调度策略依赖于底层的操作系统,因此不能保证一种跨平台的、一致的线程调度行为。因此,编写依赖于特定线程调度行为的Java程序是不推荐的。应该尽可能地编写不依赖于调度机制的并发程序。
Java的线程模型是基于原生的OS线程模型映射的,因此其调度是基于OS级别的。
当在Java代码中创建一个新的线程并启动它时,JVM会在底层操作系统中创建一个新的操作系统级别的线程,Java线程的调度和执行完全由操作系统控制。
不同的操作系统具有不同的线程调度策略和算法。而JAVA中的调度方式通常来说是属于:抢占式调度,不论是分时调度还是优先级调度都是属于抢占式调度,之所有说是抢占式调度主要是系统底层通常都是默认的抢占式调度方式。
Java线程优先级是什么?
Java线程优先级是Java线程调度器(Thread Scheduler)为线程分配处理器时间的一种相对权重。
在Java中,每个线程都有一个优先级,这个优先级可以帮助Java虚拟机(JVM)的线程调度器决定哪个线程应该被优先执行。但是这种优先级并不保证线程的执行顺序,因为线程调度器最终的决定还受到许多其他因素的影响,比如操作系统的调度策略、可用的处理器资源等。
线程的优先级被设置为一个介于Thread.MIN_PRIORITY
(1)和Thread.MAX_PRIORITY
(10)之间的整数,Thread.NORM_PRIORITY
(5),这是默认的线程优先级。
可通过调用Thread类的setPriority(int newPriority)
方法来改变线程的优先级。可以通过调用getPriority()
方法来获取线程的当前优先级。
线程优先级具有继承性。a线程启动b线程,b线程的优先级和a线程的优先级是一样的。
不同操作系统的线程调度机制可能会有所不同,因此Java线程的优先级在不同的操作系统上可能会有不同的效果。在一些系统上,优先级可能只是简单地被用作线程调度的建议,而在其他系统上,优先级可能会更直接地影响线程的执行顺序。
高优先级的线程可能会阻止低优先级的线程获得处理器时间,导致所谓的“饥饿”现象。因此,在编写多线程程序时,应该谨慎使用线程优先级,并尽量避免完全依赖线程优先级来控制程序的执行流程。
从Java 9开始,Thread类中的setPriority(int newPriority)
方法已经被标记为过时(deprecated),因为线程优先级的使用可能导致不可预测的行为,并且在现代的操作系统和JVM实现中,线程优先级的影响可能非常有限。因此,在开发新的Java应用程序时,应该考虑使用其他机制(如线程池、同步原语等)来控制并发和并行执行。
线程的生命周期有哪些状态?
通用的线程生命周期基本上可以用“五态模型”来描述:
- 创建状态(NEW)。指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
- 就绪状态(Ready)。指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
- 运行状态(Running)。当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU 的线程的状态就转换成了运行状态。
- 阻塞状态(Blocked)。运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
- 死亡状态。线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
Java 语言中线程共有六种状态:
NEW(初始化状态),新建状态,线程被创建且未启动,此时还未调用
start()
方法。Runnable/Ready(可运行 / 运行状态),Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。
WAITING(无时限等待),等待状态,处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。
以下方法会让线程陷入无限期的等待状态:
没有设置Timeout参数的
Object::wait()
方法;没有设置Timeout参数的
Thread::join()
方法;LockSupport::park()
方法。
TIMED_WAITING(有时限等待),限期等待状态,处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。
以下方法会让线程进入限期等待状态:
Thread::sleep()
方法;设置了Timeout参数的
Object::wait()
方法;设置了Timeout参数的
Thread::join()
方法;LockSupport::parkNanos()
方法;LockSupport::parkUntil()
方法。
- BLOCKED(阻塞状态),线程被阻塞,在程序等待进入同步区域的时候,线程将进入这种状态。“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。
- TERMINATED(终止状态),终止状态,表示当前线程已执行完毕或异常退出。
为什么JVM 没有区分Ready 和 Running 这两种状态呢?
JVM(Java虚拟机)没有区分Ready(就绪)和Running(运行)这两种状态,主要基于以下几个原因:
操作系统层面的抽象
线程状态的定义:线程的状态通常是从操作系统层面来划分的,但JVM中的线程状态更多地是为了反映Java程序内部线程的行为和状态,而不是直接对应操作系统的线程状态。
操作系统的差异:不同的操作系统对于线程状态的划分和管理方式可能有所不同。JVM为了提供跨平台的Java运行环境,需要对这些差异进行抽象和封装。
简化线程管理
减少复杂性:将Ready和Running状态合并,可以减少线程状态管理的复杂性。在JVM中,线程一旦进入可执行状态(即Ready状态),就等待CPU的调度来执行。这个等待和执行的过程在JVM层面被看作是一个整体,而不需要细分为Ready和Running两个状态。
提高性能:频繁地在Ready和Running状态之间切换会增加线程调度的开销。JVM通过减少状态种类,可以简化线程调度的逻辑,从而提高性能。
时间片轮转调度
- 现代操作系统的调度策略:现代操作系统架构通常都是用的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。在这种策略下,每个线程都会被分配一个时间片来执行。当时间片用完后,线程会被放回就绪队列中等待下一次调度。由于时间片通常非常短(几毫秒到几十毫秒),因此线程在RUNNABLE状态下的执行和等待时间都非常短,这使得区分Ready和Running状态变得意义不大。
综上所述,JVM没有区分Ready和Running这两种状态,主要是为了简化线程管理、提高性能,并且符合Java程序内部线程行为的描述需求。同时,也使得Java程序员能够更加方便地进行多线程编程,而不需要关心底层操作系统的线程调度细节。
线程的创建方式有哪些?
线程的创建方式在Java中主要有以下几种,每种方式都有其特点和适用场景。以下是清晰归纳的线程创建方式:
- 继承Thread类
步骤:
- 创建一个继承自Thread类的新类。
- 在新类中重写run()方法,定义线程的执行逻辑。
- 创建该类的实例。
- 通过实例调用start()方法,启动线程。
class MyThread extends Thread {
public void run() {
// 线程执行逻辑
}
}
// 创建线程的实例
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
- 实现Runnable接口
步骤:
- 创建一个实现Runnable接口的类。
- 在该类中实现run()方法,定义线程的执行逻辑。
- 创建Thread类的实例,将实现了Runnable接口的对象传递给Thread构造方法。
- 调用start()方法,启动线程。
class MyRunnable implements Runnable {
public void run() {
// 线程执行逻辑
}
}
// 创建线程的实例
Thread thread = new Thread(new MyRunnable());
// 启动线程
thread.start();
- 使用Callable接口结合FutureTask
步骤:
- 创建一个实现Callable接口的实现类。
- 实现call()方法,将此线程需要执行的操作声明在call()中,该方法可以返回执行结果。
- 创建Callable接口实现类的对象。
- 将此对象作为参数传递到FutureTask的构造器中,创建FutureTask的对象。
- 将FutureTask的对象作为参数传递到Thread的构造器中,创建Thread对象并调用start()。
- 可以通过FutureTask对象的get()方法获取子线程执行结束后的返回值。
Callable<Integer> callableTask = () -> {
// 模拟一个耗时操作
return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
Thread thread = new Thread(futureTask);
thread.start();
try {
// 获取Callable任务的执行结果
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 使用线程池(Executor框架)
步骤:
- 创建一个类实现Runnable或Callable接口。
- 实现类中重写run()或call()方法,定义线程的执行逻辑。
- 提供指定的线程池,如ThreadPoolExecutor。
- 创建实现类的对象。
- 使用线程池执行指定的线程操作。
- 关闭线程池。
ExecutorService executor = Executors.newFixedThreadPool(4); // 创建一个固定大小的线程池
executor.execute(new MyRunnable()); // 提交任务给线程池执行
// ...
executor.shutdown(); // 关闭线程池
Runnable 和 Callable 有什么区别?
Runnable和Callable是Java中用于封装任务的两种接口,它们之间存在一些关键的区别:
返回值
Runnable:没有返回值。它的run()方法执行完毕后不返回任何结果。
Callable:可以设置返回值。它的call()方法执行完毕后可以返回一个结果,这个结果可以是任意类型,需要在Callable接口实现时指定泛型类型。
异常处理
Runnable:run()方法不能抛出受检查的异常(checked exceptions),所有的异常都必须在方法内部处理。如果确实需要抛出异常,那么只能抛出运行时异常(unchecked exceptions)。
Callable:call()方法可以抛出异常,包括受检查的异常。这些异常可以被调用者捕获并处理。如果Callable任务中的call()方法抛出了异常,这些异常会被封装在Future.get()方法抛出的ExecutionException中。
使用方式
Runnable:通常用于执行那些不需要返回结果的任务。它可以被直接传递给Thread对象执行,或者作为线程池(如ExecutorService)中任务的执行体。
Callable:通常用于执行那些需要返回结果的任务。由于它不能直接传递给Thread对象执行,因此通常是将实现了Callable接口的类实例包装在FutureTask对象中,然后将FutureTask对象传递给Thread对象或线程池执行。
线程池支持
- Runnable和Callable都可以被线程池(如ExecutorService)执行。但是,对于Callable,可以拿到一个Future对象,Future对象表示异步计算的结果。当调用Future的get()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。
什么是守护线程?
守护线程(Daemon Thread),也称为后台线程或精灵线程,是一种支持型线程。
特性
- 生命周期:守护线程的生命周期与程序中用户线程的存在直接相关。当所有用户线程结束时,守护线程会随之结束,即使它们可能还在执行中。
- 设置方式:在Java中,可以通过调用线程的
setDaemon(true)
方法来将线程设置为守护线程,但这个方法必须在线程启动(即调用start()方法)之前调用。一旦线程启动,就不能再将其更改为守护线程或非守护线程。 - 优先级:守护线程的优先级通常被设置为最低,意味着在调度时,JVM会尽量先让非守护线程运行。然而,这并不意味着守护线程不会得到执行机会,只是它们的执行优先级较低。JVM 在没有非守护线程时需要立即退出,所有守护线程都将立即终止,所以JVM 退出时守护线程中的 finally 块不一定执行。
- 应用场景:守护线程非常适合执行那些不需要等待完成的任务,如垃圾回收、自动保存、定时任务、服务监控等。这些任务通常不需要人为干预,可以在程序运行期间一直存在,但在程序结束时不需要等待它们完成。
- 传递性:在Java中,如果一个线程被设置为守护线程,那么它创建的任何子线程也将自动成为守护线程。
线程通信的方式有哪些?
线程通信是并发编程中的一个重要概念,它指的是线程之间交换信息或信号以协调它们的行为。
等待/通知机制(wait/notify/notifyAll)
描述:这是Java中最基本的线程通信方式之一,通过Object类的wait()、notify()和notifyAll()方法实现。当一个线程调用了某个对象的wait()方法后,它会进入等待(阻塞)状态,直到另一个线程调用了同一个对象的notify()或notifyAll()方法将其唤醒。
特点:
只能在同步方法或同步代码块中使用。
调用wait()方法后,线程会释放锁并进入等待状态。
notify()唤醒一个等待线程,notifyAll()唤醒所有等待线程。
共享内存
描述:线程之间共享程序的公共状态,通过读写内存中的公共变量来隐式通信。这种方式在Java中非常常见,因为Java的内存模型允许堆内存中的对象在多个线程之间共享。
特点:
使用volatile关键字修饰共享变量,以确保内存可见性和防止指令重排序。
线程A对共享变量的修改会立即反映到主内存中,线程B读取时会从主内存中获取最新值。
消息传递
描述:线程之间通过发送和接收消息来显式通信。这种方式在需要解耦线程间依赖关系的场景中非常有用。
实现方式:
消息队列:通过队列数据结构实现,线程可以将消息发送到队列中,其他线程可以从队列中取出消息进行处理。
PostMessage/PostThreadMessage:在Windows编程中,线程可以通过发送消息到消息队列来进行通信。
CEvent对象:MFC(Microsoft Foundation Classes)中提供的一个同步对象,通过改变其触发状态来实现线程间的通信和同步。
并发工具类
描述:Java并发包(java.util.concurrent)提供了多种并发工具类,用于简化线程间的通信和同步。
实现方式:
CountDownLatch:允许一个或多个线程等待其他线程完成一系列操作后才继续执行。
CyclicBarrier:让一组线程互相等待,直到所有线程都达到某个公共屏障点(common barrier point)后才继续执行。
Semaphore:用于控制对共享资源的访问,允许多个线程同时访问某个资源,但总数不能超过设定的值。
管道流(Pipes)
描述:管道流是一种特殊的输入输出流,用于线程间的数据传输。Java中的PipedInputStream和PipedOutputStream可以用于实现管道通信。
特点:
一个线程将数据写入PipedOutputStream,另一个线程从PipedInputStream读取数据。
管道流是阻塞的,即如果没有数据可读,读取操作会阻塞,直到有数据写入。
线程的常用方法有哪些?
线程的控制
- start():启动线程,使线程处于就绪状态并开始执行run()方法中的任务。
- run():线程执行的任务代码。注意,直接调用run()方法并不会启动新线程,而是在当前线程中同步执行。
- setName(String name):设置线程的名称。
- getName():获取线程的名称。
- setPriority(int priority):设置线程的优先级,优先级范围从1(最低)到10(最高)。
- getPriority():获取线程的优先级。
- interrupt():中断线程。如果线程在调用Object类的wait()、Thread类的sleep()或join()方法时处于阻塞状态,或者通过调用Thread.interrupt()方法被中断,那么它将接收到一个InterruptedException。
- Thread.sleep(long millis):使当前线程休眠(暂停执行)指定的毫秒数。当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- Thread.yield():礼让线程,让出CPU资源,使其他线程有机会执行。但礼让的时间不确定,也不一定成功。因为让步的线程还有可能被线程调度程序再次选中。
线程间的通信
- wait():当前线程暂停,当前线程释放对象锁,进入等待队列,等待其他线程调用notify()或notifyAll()来唤醒。
- notify():唤醒在此对象监视器上等待的单个线程(如果有的话)。选择是任意性的。
- notifyAll():唤醒在此对象监视器上等待的所有线程。
- Thread.join():当前线程暂停,等待加入的线程运行结束,当前线程继续执行。当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程T执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态,因为join是基于wait实现的
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines),
当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)
唤醒。
注意事项
- 调用线程的start()方法会启动线程,而调用run()方法只是简单地执行该方法的代码,并不会启动新线程。
- 线程的优先级只是给操作系统一个建议,实际的调度由操作系统决定。
- 使用sleep()方法时,线程会进入阻塞状态,直到指定的时间到达。
- yield()方法只是建议当前线程让出CPU资源,实际是否让出取决于操作系统的调度策略。
- 线程间的通信和同步是并发编程中的难点,需要合理使用wait/notify、synchronized关键字、Lock接口等工具来保证线程间的正确交互和数据一致性。
为什么通常使用while循环来调用wait方法?
通常使用while循环来调用wait()方法的原因主要与线程间通信的准确性和线程安全的保证有关。
- 确保条件满足: 当线程调用wait()方法时,它会被阻塞,并等待其他线程通过调用同一个对象上的notify()或notifyAll()方法来唤醒它。然而,仅仅因为线程被唤醒了,并不意味着它应该继续执行。线程被唤醒可能是因为其他线程执行了notify()或notifyAll(),但这并不意味着之前导致线程等待的条件已经满足。因此,使用while循环而不是if语句来检查条件,可以确保在条件真正满足之前,线程不会继续执行。
- 避免虚假唤醒(Spurious Wakeup): 在Java文档中明确指出,线程可能会在没有被其他线程明确唤醒的情况下被唤醒,这被称为“虚假唤醒”。虽然这种情况不常见,但它确实可能发生。使用while循环可以确保在每次唤醒后都重新检查条件,从而防止因为虚假唤醒而导致的错误行为。
- 提高代码的健壮性: 使用while循环还可以使代码更加健壮,因为它允许在条件检查中包括额外的逻辑,比如处理异常情况或重新评估线程是否应该继续执行。这种灵活性是if语句所不具备的。
- 保持线程同步的完整性: 在调用wait()之前,通常需要获取某个对象的锁(通常是通过synchronized块或方法)。当线程被唤醒并准备继续执行时,它仍然持有这个锁。因此,使用while循环来重新检查条件并在需要时继续等待,可以确保在继续执行之前,所有必要的同步条件都得到满足。
- 符合Java的并发编程惯例: 在Java的并发编程中,使用while循环来调用wait()方法是一个被广泛接受和推荐的做法。这有助于保持代码的一致性和可预测性,同时也使得代码更易于其他开发人员理解和维护。
// 列表为空就等待
while (synchedList.isEmpty()) {
synchedList.wait();
}
sleep() 和 wait() 有什么区别?
sleep() 和 wait() 它会使此线程暂停执行指定时间,而把CPU执行机会让给其他线程,但它们之间存在多个关键的区别。
所属类和方法类型
sleep():是 Thread 类的静态方法。
wait():是 Object 类的方法。
锁的行为
sleep():在调用 sleep() 方法时,当前线程不会释放它所持有的锁。这意味着,如果有其他线程正在等待获取该锁,它们将不得不继续等待,直到当前线程从 sleep() 状态恢复并执行完毕。
wait():在调用 wait() 方法时,当前线程会释放它所持有的锁。这允许其他线程进入该对象的同步代码块或方法。当 wait() 方法返回时,线程需要再次获取锁才能继续执行。
用途和场景
sleep():通常用于暂停线程的执行,而不涉及线程间的通信或协作。它适用于需要暂停执行一段时间(如等待某个条件成立)但不希望释放锁的场景。
wait():通常用于线程间的通信和协作。当线程需要等待某个条件成立时,它会调用 wait() 方法进入等待状态,并释放锁。一旦条件成立(即其他线程调用了 notify() 或 notifyAll() 方法),线程会重新获取锁并继续执行。
异常处理
sleep():需要捕获 InterruptedException 异常。如果在调用 sleep() 方法时线程被中断,那么会抛出此异常。
wait():不需要显式捕获异常。但是,如果线程在等待过程中被中断,那么它的中断状态将被清除,并且它将立即退出 wait() 状态。然而,这并不意味着线程会立即继续执行;它仍然需要重新获取锁(如果它之前持有锁的话)。
唤醒机制
sleep():线程在指定的时间间隔结束后会自动唤醒,后返回到可运行状态,不是运行状态,不能保证该线程睡眠到期后就开始执行,如果要到运行状态还需要等待CPU调度执行,
wait():线程需要被其他线程显式唤醒(通过调用同一对象上的 notify() 或 notifyAll() 方法),或者在指定的时间超时后自动唤醒,被唤醒后进入对象锁定池。
使用条件
sleep()方法则可以放在任何地方使用。
wait()它必须放在同步控制方法或者同步语句块中使用。
sleep()方法与yield()方法有什么区别?
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行,在其他处于等待状态的线程上调用这些方法是没有意义的。所以方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
区别:
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
- 线程执行sleep()方法后会转入阻塞状态,执行sleep() 方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行。
- sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。
- sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。
notify()和 notifyAll()有什么区别?
notify()和notifyAll()都是Java中用于线程间通信的方法,它们都可以唤醒正在等待某个对象监视器(monitor)的线程,但两者之间存在几个关键的区别。
唤醒线程的数量
notify():只会随机唤醒等待队列中的一个线程。具体是哪个线程这取决于操作系统的线程调度策略。
notifyAll():**notifyAll()**会依次(FILO)调用notify唤醒该对象等待池内的中所有线程。
竞争与公平性
notify():由于只唤醒一个线程,这可能会导致其他等待的线程长时间得不到执行,特别是当唤醒的线程很快又进入等待状态时。这可能会引发线程饥饿或优先级反转等问题。
notifyAll():通过唤醒所有等待的线程,可以让它们公平竞争锁,从而在一定程度上提高线程执行的公平性和效率。
调用位置与要求
- notify()和notifyAll():都必须在同步代码块或同步方法中调用,并且调用它们的线程必须拥有该对象的监视器锁。如果线程没有拥有锁就调用这些方法,将会抛出IllegalMonitorStateException异常。
唤醒后的行为
- 被notify()或notifyAll()唤醒的线程不会立即执行,而是会返回到就绪状态,并等待重新获取锁。只有当调用notify()或notifyAll()的线程退出同步块并释放锁后,其他线程才有机会获取锁并执行。
总结
- 唤醒线程数量:notify()唤醒一个,notifyAll()唤醒所有。
- 使用场景:notify()适用于只需唤醒一个线程的场景,notifyAll()适用于需要唤醒所有线程的场景。notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中。
- 竞争与公平性:notifyAll()提供了更好的线程执行公平性和效率。
- 调用要求:两者都必须在同步块或同步方法中调用,且调用线程必须拥有该对象的监视器锁。
- 唤醒后行为:唤醒的线程需要重新获取锁后才能继续执行。
为什么 wait(),notify()和 notifyAll()必须要在同步方法或同步代码块中调用?
wait(), notify(), 和 notifyAll() 是 Java 中用于在多线程环境中实现线程间通信的重要方法,它们都属于 Object 类的方法。这些方法的设计初衷是为了在多线程环境中安全地控制线程的执行顺序和状态。它们必须在同步方法或同步代码块中调用,这主要是出于以下几个原因:
确保线程安全: wait(), notify(), 和 notifyAll() 方法依赖于 Java 的监视器(monitor)机制。每个 Java 对象都有一个与之关联的监视器,用于控制对该对象的访问。当一个线程进入某个对象的同步方法或同步代码块时,它就获得了该对象的监视器锁。这确保了在该同步块执行期间,没有其他线程可以执行该对象的另一个同步块。因此,在同步块中调用这些方法是必要的,以确保对共享资源的访问是互斥的,从而避免数据竞争和其他并发问题。
确保正确的唤醒逻辑: wait() 方法会导致当前线程暂停执行并释放锁,直到其他线程在同一对象上调用 notify() 或 notifyAll() 方法。如果 wait(), notify(), 和 notifyAll() 不是在同步块中调用,那么这些方法的调用可能无法正确地与其他线程进行通信,因为锁的状态可能不一致或无法正确管理。
监视器所有权:
当一个线程进入同步方法或同步代码块时,它获得了该对象的监视器所有权。
wait()
方法使当前线程释放它所持有的监视器,并进入等待状态。如果线程没有监视器所有权,它就不能调用 wait()。避免死锁和活锁: 同步块还帮助管理锁的获取和释放,防止死锁和活锁的发生。死锁是指两个或多个线程互相等待对方释放锁,导致它们都无法继续执行。活锁则是指线程不断尝试获取锁但总是失败,因为其他线程也在尝试获取锁。在同步块中正确地使用 wait(), notify(), 和 notifyAll() 可以帮助避免这些问题。
符合 Java 内存模型(JMM): Java 内存模型定义了线程和主内存之间的交互方式,包括如何同步数据。在同步块中调用这些方法可以确保内存可见性和有序性,即一个线程对共享变量的修改对其他线程是可见的,并且操作按照预期的顺序执行。
综上所述,wait(), notify(), 和 notifyAll() 方法必须在同步方法或同步代码块中调用,以确保线程安全、正确的唤醒逻辑、避免死锁和活锁,以及符合 Java 内存模型的要求。
线程的 run()和 start()有什么区别?
在Java中,线程(Thread)是执行程序的一条执行路径。理解run()和start()方法的区别对于正确使用线程至关重要。
run() 方法
- run()方法是Thread类的一个方法,用于定义线程执行体,被称为线程体。
- 当线程启动时,此线程会调用run()方法来执行其任务。
- 直接调用线程对象的run()方法并不会启动新的线程,而是会在调用该方法的线程(通常是主线程)中同步执行run()方法中的代码。
- run()方法只是一个普通的实例方法,它的行为完全取决于如何被调用。
start() 方法
- start()方法是Thread类的一个方法,用于启动线程。
- 当你调用线程的start()方法时,会启动一个新的线程来执行该线程的run()方法。
- start()方法负责创建线程并调用run()方法,但它是在一个新的线程中调用run()方法,而不是在调用start()的线程中。
- 每个线程只能被启动一次。一旦线程被启动,它的执行将由Java虚拟机(JVM)调度和执行。
结论
- run()方法定义了线程的执行体,但它本身并不启动线程。
- start()方法用于启动线程,它会创建新的线程来执行run()方法中的代码。
- 直接调用run()方法并不会启动新线程,而start()方法会。
终止线程的方法有哪些?
在Java中,终止线程的方法有多种,每种方法都有其特定的使用场景和注意事项。以下是一些常见的终止线程的方法:
使用退出标志(推荐方式)
- 通过设置一个共享变量(通常是一个布尔类型的volatile变量),在线程的执行循环中检查这个变量的值。如果变量为真(或假,取决于具体实现),则退出循环,从而终止线程的执行。这种方法的好处是线程可以正常完成其清理工作,如关闭文件、释放资源等,避免了数据不一致和资源泄露的问题。
使用interrupt()方法
调用线程的interrupt()方法会将该线程的中断状态设置为true。线程可以周期性地检查自身的中断状态(通过调用isInterrupted()方法),并据此作出相应的处理,如退出循环、释放资源等。如果线程在阻塞状态(如调用sleep()或wait()方法时)被中断,会抛出InterruptedException异常。
注意:interrupt()方法本身并不会立即停止线程的执行,而是给线程发送一个中断信号。线程需要自行检查并响应这个中断信号。
使用stop()方法(不推荐)
- 当用stop()来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他线程将会“看”到这个不一致的状态,这可能会导致程序执行的不确定性,并且这种问题很难被定位。鉴于以上方法的不安全性,因此 stop() 方法已经被标记为过时,不建议使用该方法来终止线程了。
使用守护线程(Daemon Thread)
- 守护线程是一种特殊的线程,当程序中所有非守护线程都结束时,守护线程会自动终止。然而,守护线程主要用于执行后台任务,如垃圾回收等,而不是用于常规的业务逻辑处理。因此,将业务线程设置为守护线程来终止其执行并不是一种推荐的做法。
使用线程池(ThreadPool)
- 线程池提供了一种更高级别的线程管理方式。通过调用线程池的shutdown()或shutdownNow()方法,可以终止线程池的执行,并等待已提交的任务完成。然而,这并不会直接终止单个线程的执行,而是管理一组线程的执行。
推荐使用设置退出标志的方法来终止线程,因为它既安全又可靠。其他方法如interrupt()也可以用于终止线程,但需要线程本身能够响应中断信号。而stop()方法由于存在安全隐患,已被废弃并不推荐使用。
Thread.interrupted()方法的作用是什么?
Thread.interrupted()方法是Java中Thread类的一个静态方法,其主要作用及特点如下:
作用
- 判断当前线程是否被中断:Thread.interrupted()方法用于判断当前线程(即调用该方法的线程)是否被中断。如果线程在调用此方法之前已被中断(即其中断状态为true),则此方法将返回true。
- 清除中断状态:重要的是,调用Thread.interrupted()方法后,当前线程的中断状态会被清除,即将中断状态重新设置为false。如果连续两次调用此方法,且线程在第一次调用后被中断,那么第一次调用将返回true,而第二次调用将返回false,因为第一次调用已经清除了中断状态。
使用注意事项
- 谨慎使用:由于Thread.interrupted()方法会清除中断状态,因此在多线程编程中需要谨慎使用。如果需要在判断线程是否中断的同时保留中断状态,建议使用isInterrupted()方法,因为isInterrupted()方法仅检查中断状态而不清除它。
- 应用场景:Thread.interrupted()方法通常用于需要立即响应中断并清除中断状态的情况。例如,在循环中检查中断状态,并在发现中断时退出循环并清除中断状态,以避免重复处理中断。
- 中断处理:在Java中,中断是一种协作机制,而不是强制终止机制。线程需要定期检查其中断状态,并根据需要作出响应。例如,如果线程在执行阻塞操作时(如sleep()、wait()或join())被中断,则会抛出InterruptedException异常。线程可以捕获此异常,并据此执行相应的中断处理逻辑。
如何捕获一个线程抛出的异常?
线程通常是通过实现Runnable接口或继承Thread类来创建的。然而,无论是哪种方式,直接捕获由线程内部执行的代码抛出的异常都不是非常直接,因为线程的执行是异步的,并且run()方法的调用者(对于Runnable)或start()方法的调用者(对于Thread)通常不会直接接收到run()方法内部抛出的异常。
有几种方法可以捕获或处理线程中抛出的异常:
使用try-catch块
- 在run()方法内部使用try-catch块来捕获并处理可能抛出的异常。这种方法可以确保异常在线程内部被捕获,但可能无法将异常信息传递给线程外部的处理逻辑(除非通过某种形式的通信机制,如共享变量、事件监听等)。
通过共享资源
- 可以在线程外部定义一个共享资源(如List<Throwable>),并在run()方法内部将捕获的异常添加到这个共享资源中。然后,你可以在主线程或其他线程中定期检查这个共享资源以获取异常信息。
使用Future和Callable
- 如果你使用的是ExecutorService来管理线程,可以考虑使用Callable接口而不是Runnable接口。Callable允许你返回一个结果,并且可以抛出异常。你可以将Callable任务提交给ExecutorService,并使用Future对象来获取结果或捕获异常。
使用UncaughtExceptionHandler
- 可以使用公共静态接口
Thread.UncaughtExceptionHandler
完成,该接口是当线程因未捕获的异常而突然终止时调用的处理程序接口。当一个线程由于未捕获的异常而即将终止时,Java虚拟机将使用getUncaughtExceptionHandler()
来查询线程的UncaughtExceptionHandler
,并将调用该处理程序的 uncaughtException方法,并将该线程和异常作为参数传递。如果某个线程没有 显式设置其UncaughtExceptionHandler,则其ThreadGroup对象将充当其 UncaughtExceptionHandler。如果ThreadGroup对象没有处理异常的特殊要求,它可以将调用转发到默认的未捕获异常处理程序。可通过setUncaughtExceptionHandler()
方法设置自定义处理器
- 可以使用公共静态接口
ThreadLocal 是什么?
ThreadLocal是Java中的一个类,它被称为线程变量。
主要特点:
- 线程隔离:ThreadLocal为每个使用该变量的线程都创建了一个独立的变量副本,这些副本之间是相互隔离的。一个线程对其ThreadLocal变量的修改不会影响到其他线程的ThreadLocal变量。主要用于一个线程内跨类、方法传递数据。
- 减少同步:由于每个线程都拥有自己的变量副本,因此在某些情况下,使用ThreadLocal可以减少同步的需求,从而提高程序的性能。
- 内存开销:虽然ThreadLocal为每个线程都创建了一个变量副本,但这并不意味着它会增加太多的内存开销。因为只有当线程实际访问ThreadLocal变量时,才会为其创建副本。而且,当线程结束时,其ThreadLocal变量副本也会被垃圾回收器回收。
实现原理
ThreadLocalMap 的引入
核心数据结构:每个线程在 Java 中都有一个与之关联的 ThreadLocalMap 对象。这个 ThreadLocalMap 是一个定制的哈希映射(HashMap 的变种),用于存储该线程所有的 ThreadLocal 变量及其对应的值。
存储方式:ThreadLocalMap 的键(Key)是 ThreadLocal 对象本身的弱引用(WeakReference),而值(Value)则是 ThreadLocal 变量所持有的数据对象。这种设计使得每个线程都能独立地访问和操作自己的数据副本,而不会相互干扰。
线程隔离的实现
- 独立的数据副本:当线程访问一个 ThreadLocal 变量时,它实际上是通过自己的 ThreadLocalMap 来获取这个变量的值。由于每个线程的 ThreadLocalMap 是相互独立的,因此它们之间的数据也是隔离的。
主要方法:
set()
方法:首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就直接设置值,key 是当前的 ThreadLocal 对象,value 是传入的参数。如果 map 不存在就通过 createMap 方法为当前线程创建一个 ThreadLocalMap 对象再设置值。get()
方法:首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就以当前ThreadLocal对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。如果 e 不存在或者 map 不存在,先为当前线程创建一个 ThreadLocalMap 对象然后返回默认的初始值 null。remove()
方法:首先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 m,如果 m 不为空,就解除 ThreadLocal 这个 key 及其对应的 value 值的联系。
弱引用的使用
避免内存泄漏:由于 ThreadLocalMap 的键是 ThreadLocal 对象的弱引用,因此在没有其他强引用指向 ThreadLocal 对象时,该对象可能会被垃圾回收器回收。这种设计有助于减少内存泄漏的风险,因为即使 ThreadLocal 对象被回收,其对应的 ThreadLocalMap 中的条目仍然可以通过遍历和清理来释放内存。
显式清理:然而,仅仅依靠弱引用并不足以完全避免内存泄漏。因此,建议在使用完 ThreadLocal 变量后显式调用 remove() 方法来清除数据,以确保 ThreadLocalMap 中的条目能够被及时回收。
哈希冲突的解决
哈希表的变体:与普通的 HashMap 不同,ThreadLocalMap 的实现更加紧凑和高效。它使用开放寻址法(Open Addressing)来解决哈希冲突,即当两个键的哈希值相同时,它们会存储在数组中的不同位置,并通过链表或其他数据结构来链接这些位置上的条目。
性能优化:为了提高性能,ThreadLocalMap 还采用了一些优化措施,如懒加载(Lazy Loading)和延迟清理(Lazy Cleanup)。这意味着 ThreadLocalMap 不会在每次访问时都进行清理操作,而是会在必要时(如内存不足时)进行清理。
ThreadLocal 使用场景有哪些?
ThreadLocal 的使用场景主要集中在需要确保每个线程都能够独立地拥有自己的变量副本的场景中。
会话管理:
- 在 Web 应用中,每个用户的请求通常都是由一个独立的线程来处理。可以使用 ThreadLocal 存储用户会话信息(如用户ID、权限等),这样在整个请求处理过程中,任何组件都可以轻松地访问这些信息,而无需将信息作为参数传递给每个方法。
数据库连接:
- 在多线程环境中管理数据库连接时,可以使用 ThreadLocal 为每个线程提供一个独立的数据库连接。这样可以避免线程间的数据库连接共享问题,同时简化连接的获取和释放过程。
事务管理:
- 在需要事务支持的场景中,每个线程可能都有自己独立的事务上下文。使用 ThreadLocal 可以存储每个线程的事务信息,如事务ID、事务状态等,以便在需要时进行事务的回滚或提交。
日志记录:
- 在分布式系统中,为了追踪请求的完整路径,通常需要在日志中记录请求ID。使用 ThreadLocal 存储请求ID可以确保在整个请求处理过程中,任何日志记录操作都能够获取到正确的请求ID。
安全信息:
- 在涉及安全性的应用中,如需要记录用户的登录信息或权限信息,可以使用 ThreadLocal 来存储这些信息。这样,在整个请求处理过程中,任何需要这些信息的地方都可以方便地访问到。
长生命周期的复杂对象:
- 对于那些创建和销毁成本较高,且每个线程只需要一个实例的复杂对象(如大型缓存、连接池等),可以使用 ThreadLocal 为每个线程提供一个独立的实例。这样可以避免在多线程环境下对这些对象的并发访问控制,提高性能。
上下文传递:
- 在某些情况下,方法调用链中的多个方法都需要访问某个上下文信息(如当前用户、环境配置等)。如果这些信息不是方法参数的一部分,那么可以使用 ThreadLocal 来传递这些信息,避免在每个方法调用时都将其作为参数传递。
ThreadLocal 可能存在哪些问题?
不正确使用 ThreadLocal,也可能会导致一些问题。以下是 ThreadLocal 可能存在的几个主要问题:
内存泄漏
内存泄漏是 ThreadLocal 使用中最常出现的问题之一。ThreadLocal 的内部实现是通过一个 ThreadLocalMap 来存储每个线程的变量值,其中 ThreadLocal 作为 key,变量值作为 value。ThreadLocalMap 的 key 是 ThreadLocal 的弱引用,而 value 是强引用。这意味着如果 ThreadLocal 对象没有被外部强引用引用,那么它可能会在垃圾回收时被回收,但其对应的 value 对象由于 ThreadLocalMap 的强引用而不会被回收,从而导致内存泄漏。
解决方案:
- 在使用完 ThreadLocal 变量后,及时调用 remove() 方法清除它,将 ThreadLocalMap 中对应的键值对删除,从而避免内存泄漏。
- 尽量避免在 ThreadLocal 中存储大对象或长时间持有的对象。
脏读(数据污染)
脏读问题主要出现在使用线程池的场景中。由于线程池中的线程是复用的,如果线程在执行任务时没有正确地清理 ThreadLocal 变量,那么下一个任务可能会读取到上一个任务留下的数据,从而导致数据污染。
解决方案:
在使用线程池时,确保在每个任务执行完毕后都调用 remove() 方法清除 ThreadLocal 变量。
可以通过在任务执行前后显式地调用 set() 和 remove() 方法来管理 ThreadLocal 变量的生命周期。
不可继承性
ThreadLocal 变量默认是不可继承的,即子线程无法继承父线程中的 ThreadLocal 变量值。虽然 Java 提供了 InheritableThreadLocal 类来解决这个问题,但 InheritableThreadLocal 并不能实现不同线程之间的数据共享,它只是在创建子线程时,将父线程中的 InheritableThreadLocal 变量值传递给子线程。
解决方案:
根据需要选择使用 ThreadLocal 或 InheritableThreadLocal。
如果需要在子线程中访问父线程的数据,可以考虑使用其他方式(如通过构造函数传递参数)来实现。
初始值问题
ThreadLocal 并不提供默认的初始值。如果尝试访问一个尚未设置值的 ThreadLocal 变量,将会得到 null。虽然可以通过重写 initialValue() 方法来提供一个初始值,但这通常不是必要的,因为更常见的做法是在需要时显式地调用 set() 方法来设置值。
解决方案:
根据需要选择是否重写 initialValue() 方法来提供初始值。
在使用 ThreadLocal 变量之前,确保已经通过 set() 方法设置了值。
什么是虚拟线程?
虚拟线程(Virtual Threads)是Java平台中引入的一种新型线程模型,旨在解决传统平台线程(也称为操作系统线程或用户线程)在资源使用和并发性能上的限制。以下是虚拟线程实现原理的详细解析:
一、基本概念
- 平台线程:在Java中,传统的线程实现是通过java.lang.Thread类来创建的,这些线程直接映射到操作系统的线程上,由操作系统进行调度和管理。这种线程通常被称为平台线程或用户线程。
- 虚拟线程:虚拟线程是Java虚拟机(JVM)内部实现的一种轻量级线程,它们不直接映射到操作系统的线程上,而是由JVM的调度器进行管理和调度。虚拟线程的设计旨在减少线程创建和销毁的开销,提高并发性能。
二、实现原理
轻量级线程管理
虚拟线程的创建和销毁由JVM内部完成,无需操作系统介入,因此创建和销毁的开销远小于平台线程。
虚拟线程的堆栈存储在Java堆内存中,而不是传统的本地线程堆栈中,这显著减少了每个线程所需的初始内存占用。
协作调度模型
虚拟线程采用协作调度模型,即线程在执行过程中主动释放CPU,允许其他线程执行。这种调度方式减少了锁竞争和上下文切换的开销。
当虚拟线程遇到阻塞操作(如I/O等待)时,它可以释放执行权,允许其他虚拟线程在相同的平台线程上继续执行。这种机制提高了系统的响应性和吞吐量。
载体线程
虚拟线程并不直接执行在物理CPU上,而是需要通过载体线程(Carrier Thread)来执行。载体线程是JVM内部维护的一组平台线程,它们负责执行虚拟线程中的任务。
当虚拟线程需要执行时,JVM会将其调度到一个可用的载体线程上执行。载体线程的数量通常与CPU核心数相关,但可以根据需要进行调整。
动态堆栈管理
- 虚拟线程的堆栈大小是动态可变的,这意味着JVM可以根据需要为虚拟线程分配或释放堆栈空间。这种机制避免了传统平台线程中固定堆栈大小带来的限制。
API支持
Java提供了专门的API来支持虚拟线程的创建和管理。例如,可以通过Thread.ofVirtual()静态工厂方法来创建虚拟线程。
此外,Java还提供了java.util.concurrent.ExecutorService的实现(如ThreadPerTaskExecutor),用于管理和执行虚拟线程池中的任务。
三、性能优势
- 低开销:虚拟线程的创建和销毁开销远低于平台线程,这使得在需要大量线程的场景下可以显著减少资源消耗。
- 高并发:由于虚拟线程的数量可以远超过物理CPU核心数,因此它们能够支持更高的并发级别。
- 高响应性:虚拟线程的协作调度模型和动态堆栈管理使得系统能够更好地处理阻塞操作和上下文切换,提高了程序的响应性。
四、应用场景
- 虚拟线程特别适用于高并发、高吞吐量的应用场景,如Web服务器、数据库连接池、异步IO处理等。
- 在这些场景中,虚拟线程能够显著提高系统的性能和资源利用率。
总之,虚拟线程是Java平台中引入的一种创新线程模型,它通过轻量级线程管理、协作调度模型、载体线程和动态堆栈管理等机制实现了高效的并发处理。随着Java平台的不断发展和完善,虚拟线程将在更多领域得到广泛应用。
虚拟线程和平台线程有什么关系?
虚拟线程(Virtual Threads)和平台线程(Platform Threads)在Java平台中扮演着不同的角色,但它们之间存在密切的关系和互动。
映射关系:
虚拟线程与平台线程之间存在“多对一”的映射关系。多个虚拟线程可以映射到一个平台线程上,当虚拟线程需要执行时,JVM会将其调度到对应的平台线程上执行。
这种映射关系使得虚拟线程能够充分利用系统资源,提高并发性能。同时,由于虚拟线程的轻量级特性,它们能够支持大规模的并发操作,而不会对系统造成过大的负担。
调度与执行:
虚拟线程的调度和执行完全由JVM负责,不需要操作系统内核的参与。JVM内部的调度器会根据系统的负载和虚拟线程的优先级等因素来调度虚拟线程的执行。
当虚拟线程被调度到平台线程上执行时,它会占用平台线程的资源来执行自己的任务。当虚拟线程被阻塞或等待时,平台线程可以切换到执行其他虚拟线程或执行其他任务。
生命周期与状态:
虚拟线程和平台线程都具有自己的生命周期和状态。虚拟线程的状态包括初始状态、启动状态、可执行状态、运行状态、阻塞状态等。平台线程的状态则与操作系统的线程状态相对应。
虚拟线程的生命周期由JVM管理,当虚拟线程执行完毕或被取消时,JVM会负责销毁该虚拟线程并回收其资源。平台线程的生命周期则与操作系统的线程生命周期相对应。
虚拟线程有什么优点和缺点?
优点
创建成本低:
虚拟线程的创建和销毁开销远小于平台线程(即传统的操作系统线程)。这是因为虚拟线程的创建和销毁完全在JVM层面进行,不需要操作系统的介入,从而避免了操作系统层面上的复杂性和开销。
这一优点使得在需要大量创建和销毁线程的场景中,使用虚拟线程可以显著减少内存和CPU资源的消耗,提高系统的整体性能。
切换开销小:
虚拟线程的上下文切换由JVM内部的调度器管理,不需要操作系统的介入,因此上下文切换的开销极低。
这使得在高并发场景下,虚拟线程能够更高效地利用CPU资源,提高系统的响应速度和吞吐量。
资源利用率高:
虚拟线程可以支持大规模的并发操作,因为它们不受操作系统线程数量限制的限制。这意味着即使系统资源有限,虚拟线程也能通过高效的调度和管理来充分利用这些资源。
同时,由于虚拟线程的数量可以非常庞大,因此可以支持更多的并发请求或任务,提高系统的整体并发性能。
更好的可伸缩性:
- 由于虚拟线程的轻量级特性,Java应用程序可以更容易地扩展到更多的并发用户或任务。这使得虚拟线程成为处理大规模并发场景的理想选择。
简化并发编程:
- 虚拟线程的出现使得并发编程变得更加简单和直观。开发者可以更容易地编写出高效、可扩展的并发应用程序,而无需过多关注底层线程的复杂性和开销。
缺点
资源耗尽的风险:
- 尽管虚拟线程可以创建大量线程,但过多的线程仍可能导致性能下降或资源耗尽。因此,在设计应用程序时,需要合理控制并发度,避免创建过多的虚拟线程。
依赖JVM的实现:
- 虚拟线程的性能和表现高度依赖于JVM的实现和配置。不同的JVM实现和配置可能会对虚拟线程的性能产生显著影响。因此,开发者需要针对特定的JVM环境和应用场景进行优化和调整。
不适用于计算密集型任务:
- 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。
创建虚拟线程的方法?
Java 21 已经正式支持虚拟线程,官方提供了以下四种方式创建虚拟线程:
使用
Thread.startVirtualThread()
方法最直接和简洁的方法,用于创建并立即启动一个虚拟线程。该方法接受一个实现了Runnable接口的对象作为参数,该对象代表了线程需要执行的任务。这种方法非常适用于需要立即启动线程并执行任务的场景。
Runnable task = () -> {
System.out.println("虚拟线程正在执行任务...");
};
Thread.startVirtualThread(task);
使用
Thread.ofVirtual()
方法Thread.ofVirtual()`方法提供了更多的灵活性,允许开发者在创建虚拟线程时设置一些属性,如线程名称等。然后,可以通过调用start(Runnable task)方法来启动线程。
Thread virtualThread = Thread.ofVirtual().name("my-virtual-thread").start(task);
可以先通过unstarted(Runnable task)方法创建一个未启动的虚拟线程实例,然后手动调用start()方法启动它。
Thread virtualThread = Thread.ofVirtual().name("my-virtual-thread").unstarted(task);
virtualThread.start();
使用虚拟线程工厂
通过Thread.ofVirtual().factory()方法,可以创建一个虚拟线程工厂(ThreadFactory),该工厂用于生成虚拟线程实例。这种方法允许开发者对虚拟线程进行更细致的配置和管理。
ThreadFactory virtualThreadFactory = Thread.ofVirtual().name("my-virtual-thread-").factory();
Thread virtualThread = virtualThreadFactory.newThread(task);
virtualThread.start();
使用基于虚拟线程的ExecutorService
基于虚拟线程的ExecutorService实现,如Executors.newVirtualThreadPerTaskExecutor()。这种方法特别适用于需要管理大量并发任务的场景,因为它能够自动管理虚拟线程池,并根据需要创建和销毁线程。
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(task);
// 记得在所有任务完成后关闭ExecutorService
executor.shutdown();