线程和进程是操作系统中两个重要的概念,用于实现并发执行和多任务处理。
基础概念
进程
进程(Process):进程是计算机中正在运行的程序的实例。它是操作系统分配系统资源的基本单位,包括程序代码、数据、打开的文件、内存空间、进程状态等。
每个进程都有独立的内存空间和资源,彼此之间相互隔离、互不影响。进程之间通常通过进程间通信(IPC)机制进行数据交换和通信。
任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。主线程的结束会导致进程的结束,进程使用内存地址可以限定使用量
线程
线程(Thread):线程是进程中的一个执行单元,是 CPU 调度的最小单位。一个进程可以包含多个线程,在同一进程内的线程共享相同的内存空间和资源。
线程之间通过共享内存来进行通信,可以更高效地进行数据交换。线程拥有独立的栈空间,但共享进程的堆空间。
二者的联系
线程和进程是相关且互相依赖的概念,可以说线程是进程的执行单元。
在一个程序运行时,操作系统会创建一个进程来承载程序的执行。进程作为操作系统分配资源的基本单位,每个进程都有独立的内存空间、文件资源、处理器等。
而在一个进程内部,可以有多个线程同时执行。线程是进程内的执行单元,可以看作是进程的一个分支。在一个进程下的多个线程共享相同的内存空间,包括代码段、数据段和堆段等。因此,线程之间可以直接访问和共享进程的资源和数据。
线程的创建、销毁和切换的开销远小于进程,所以多线程的并发执行比创建多个进程来实现并发更高效。多线程可以提高程序的效率和响应时间,因为多个线程可以同时执行不同的任务。
然而,多线程的同时执行也带来了一些问题,如线程之间共享数据的同步、避免竞态条件和死锁等。因此,在多线程编程中需要使用同步机制(如锁)和线程之间的通信机制(如等待/通知机制)来确保线程之间的安全协作。
二者的区别
-
进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即 CPU 分配时间的单位。
-
资源分配:每个进程都有独立的资源分配,包括内存空间、文件句柄等;而线程是在进程内部共享资源的,包括内存和文件等。
-
线程切换和创建销毁开销:线程是轻量级的,创建和销毁线程开销相对较小,只需要保存寄存器和栈信息。
而创建和销毁进程开销相对较大,不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度。同时,线程的切换也比进程的切换开销要小。
-
通信和同步:线程之间可以更方便地进行通信和同步,共享数据更直接;而进程之间需要通过特定较为复杂的通信机制进行数据交换。
-
安全性:线程共享内存,需要避免竞态条件和数据不一致等并发问题,一个线程崩溃可能影响整个程序的稳定性
而进程相互独立,不会受到其他进程的影响。进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,安全性相对较高。
最后,需要注意的是,在多核处理器中,不同的线程可以被调度到不同的 CPU 核心上并行执行,从而实现真正的并发执行。
线程
线程的生命周期
线程的主要类和接口
Thread 类
Thread 类是 Java 提供的用于创建和管理线程的基本类。它是 Java 并发编程的核心之一,位于 java.lang 包中。
Thread 类中还提供了一些其他常用的方法,例如:
-
start()
:启动线程,然后由 JVM 调用此线程的run()
方法。 -
sleep(long millis)
:使当前线程暂停指定的时间。 -
yield()
:让出当前线程的执行时间片,使得其他线程有机会执行。 -
join()
:等待线程的结束,阻塞当前线程直到目标线程执行完毕。 -
interrupt()
:中断线程的执行。 -
getName()
和setName()
:获取和设置线程的名称。
Runnable 接口
Runnable
接口是 Java 并发编程中的一个重要接口,用于定义一个可以被线程执行的任务。它是一个函数式接口,只包含一个无参数无返回值的 run() 方法。
Runnable
接口定义如下:
public interface Runnable {
void run();
}
实现 Runnable
接口的类需要实现 run()
方法,该方法中定义了线程要执行的任务逻辑。
使用 Runnable
接口创建的线程可以更好地实现代码的重用性,因为它可以继续扩展其他类或实现其他接口。而且,Java 中的多继承是不允许的,因此通过实现 Runnable
接口可以在不影响类继承关系的情况下创建线程。
适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。
run 方法
run()
方法是 Java 线程执行任务的入口,默认的 run()
方法不会做任何事,通过实现 Runnable
接口并实现其中的 run()
方法或者继承 Thread 类并重写其中的 run()
方法完成线程的创建和任务的执行。
run()
方法是一个无参数、无返回值的方法,线程启动后会从 run()
方法开始执行线程的任务,并按照代码编写的逻辑执行其中的语句。
在实际编写多线程程序时,应注意以下几点:
-
run()
方法不要直接调用,应该调用 Thread 的 start() 方法来启动线程。直接调用 run() 方法是在当前线程中执行该方法,而并不会创建新的线程。 -
run()
方法中的语句应该尽可能的以线程安全的方式编写,避免发生数据竞争或资源争夺。 -
run()
方法中的语句应该尽可能的简洁明了,避免出现复杂的控制结构或方法调用,以便在多线程环境下更容易地调试和维护代码。
Callable 接口
Callable
接口是 Java 并发编程中的一个重要接口,它定义了一个有返回值且可能会抛出异常的任务。与 Runnable
接口不同,Callable
接口的 call()
方法可以返回一个值,而 Runnable
接口只能执行任务,无法返回值。
Callable
接口定义如下:
public interface Callable<V> {
V call() throws Exception;
}
- 可以看到,Callable 接口使用泛型
V
指定返回值的类型,而 call() 方法可以抛出 Exception 异常。
当我们需要并发执行一些任务并获取它们的执行结果时,就可以使用 Callable 接口。
在 Java 中,可以使用 Future
接口配合 Callable
接口来实现获取并发任务的结果。Future
接口表示异步计算的结果,提供了一些方法来获取计算结果、取消任务等操作。
Future 接口
Future 接口是 Java 多线程编程中,一个非常重要的接口,代表了异步任务的执行结果。通过 Future 接口,我们可以在一个线程执行异步任务的同时,保持另一个线程对异步任务执行进度的监控和控制。
接口声明
Future 接口位于 java.util.concurrent
包下
Future
提供了三种功能:
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果
主要方法
Future
接口的主要方法如下:
-
boolean cancel(boolean mayInterruptIfRunning)
取消当前异步任务的执行,如果可能的话,会中断正在执行中的任务。如果取消任务成功,此方法返回 true,否则返回 false。若任务已完成,则必定返回 false
mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务,如果设置 true,则表示可以取消正在执行过程中的任务。
-
boolean isCancelled()
查询当前异步任务是否已经被取消。若任务已取消则返回 true,否则返回 false。 -
boolean isDone()
查询当前异步任务是否已经完成。若任务已完成则返回 true,否则返回 false。 -
V get()
获取异步任务的执行结果,该方法会阻塞当前线程,直到任务执行完成并返回结果。如果任务被取消或执行过程中发生异常,则抛出对应的异常。 -
V get(long timeout, TimeUnit unit)
除了获取任务的执行结果外,该方法还允许我们设置超时时间,即在规定时间内等待任务执行结果,如果任务在规定时间后仍未执行完毕,则抛出 TimeoutException 异常。
Future 接口的实现类是 FutureTask
,它实现了 Future
接口和 Runnable
接口,因此它不仅可以作为异步任务的执行结果,还可以作为线程执行的任务,被提交给 ExecutorService
等线程池进行执行。
FutrueTask
FutureTask
是 Future
的唯一实现类
FutureTask
是一个实现了 RunnableFuture
接口的类,它既可以作为 Runnable
运行,也可以作为 Future
获取结果。
在多线程环境下,FutureTask
可以用来包装一个 Callable
或 Runnable
对象,并提供一个额外的 get()
方法用于获取执行结果,该方法会阻塞等待任务执行完成并返回结果。
FutureTask 提供了 2 个构造器:
RunnableFuture
RunnableFuture
接口是 Future
和 Runnable
接口的结合,它是一个可取消、可运行的异步任务,可以在提交给 Executor
执行器中执行。
RunnableFuture
接口继承自 Runnable
和 Future
接口,其中 Runnable
接口表示一个可以在独立线程中运行的任务,而 Future
接口允许我们检查任务是否已经完成,还可以获取任务完成时的结果。
RunnableFuture 方法
RunnableFuture
接口定义了两个方法:
-
void run()
:执行该任务的主体方法,其必须实现自定义的任务逻辑,并能够自动终止任务或响应任务取消请求。 -
boolean cancel(boolean mayInterruptIfRunning)
:尝试取消该任务的执行,如果任务已完成、已取消或者无法取消,则返回false
,否则返回true
。参数mayInterruptIfRunning
表示是否中断正在执行的线程,通常情况下设置为true
。
如果一个类实现了 RunnableFuture
接口,那么它就可以被 Executor
执行器提交执行。执行器将会在一个新线程中调用 RunnableFuture
实例的 run()
方法,并使用 Future
接口追踪任务的执行状态。
需要注意的是,RunnableFuture
更适合实现长时间运行的任务,因为它具有取消任务的能力。如果任务执行时间很短,可以考虑使用 Runnable
接口或 Callable
接口代替。
操作线程
创建线程
继承 Thread 类
继承 Thread 类
:创建一个类,继承自 Thread 类,并重写 run() 方法,该方法中定义了线程要执行的任务逻辑。然后通过实例化该类,并调用 start()
方法来启动线程。
示例:
class MyThread extends Thread {
public void run() {
// 线程执行的任务逻辑
// ...
}
}
// 创建并启动线程
MyThread myThread = new MyThread();
myThread.start();
实现 Runnable 接口
实现 Runnable 接口
:创建一个实现了 Runnable 接口的类,并实现 run() 方法。然后通过创建 Thread 类的实例,将该实现了 Runnable 接口的类作为构造参数传递给 Thread 类的构造方法,再调用 start() 方法来启动线程。
示例:
class MyRunnable implements Runnable {
public void run() {
// 线程执行的任务逻辑
// ...
}
}
// 创建实现了 Runnable 接口的类的实例
MyRunnable myRunnable = new MyRunnable();
// 创建线程对象,并将 runnable 对象做为构造参数传递
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
实现 Callable 接口
- 实现
Callable
接口,重写call
方法,这种方式可以通过 FutureTask 获取任务执行的返回值。
public class CallerTask implements Callable<String> {
public String call() throws Exception {
return "Hello,i am running!";
}
public static void main(String[] args) {
//创建异步任务
FutureTask<String> task=new FutureTask<String>(new CallerTask());
//启动线程
new Thread(task).start();
try {
//等待执行完成,并获取返回结果
String result=task.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 借助线程池:
-
创建一个类,实现
Callable
接口并指定泛型类型,该泛型指定了异步任务的返回结果类型。例如,如果异步任务返回的是整数类型,则可以创建一个类MyCallable implements Callable<Integer>
。 -
实现
call()
方法,在该方法中编写异步任务的逻辑代码,并返回任务的执行结果。 -
创建一个
ExecutorService
实例,用于提交异步任务并管理线程的执行。 -
创建
Callable
对象的实例。 -
调用
ExecutorService
的submit()
方法,将Callable
对象作为参数,提交异步任务。 -
调用
Future
对象的相关方法,获取异步任务的执行结果或进行其他操作。
-
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
return 1 + 2;
}
}
public class Main {
public static void main(String[] args) {
// 创建一个可重用线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 创建 Callable 对象的实例
Callable<Integer> callable = new MyCallable();
// 提交异步任务并获取 Future 对象
Future<Integer> future = executor.submit(callable);
// 关闭线程池,不再接收新的任务
executor.shutdown();
try {
// 获取异步任务的执行结果
int result = future.get();
System.out.println("任务执行结果:" + result);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
线程常见方法
start()
start()
是线程的方法,用于启动一个新的线程并开始执行线程的任务。
在 Java 中,线程是通过创建 Thread
类的实例来表示的。当创建了一个线程对象后,可以调用该线程对象的 start()
方法来启动线程。
调用线程的 start()
方法后,新线程会被创建,并在新线程中执行线程的 run()
方法。run()
方法中定义了线程的具体任务逻辑。如果没有调用 start()
方法,只是直接调用 run()
方法,则线程的任务逻辑将在当前线程中执行,并不会创建新的线程。
需要注意的是,在调用 start()
方法后,线程不会立即执行,而是处于 NEW
状态,等待系统的调度。在适当的时机,系统会安排线程的执行,并进入到 RUNNABLE
状态,开始执行线程的任务。
另外,线程的 start()
方法只能被调用一次,如果多次调用会抛出 IllegalThreadStateException
异常。一旦线程执行完毕或被中断,就无法再次启动该线程。
sleep()
sleep()
方法将当前线程置于阻塞状态,让出CPU资源,等待睡眠时间结束后再重新进入就绪状态。
sleep方法使用的是绝对时间,可以指定一个时间参数,表示当前线程需要暂停执行的时间长度,单位是 ms
。
sleep方法的语法如下:
public static void sleep(long millis) throws InterruptedException
-
millis
参数表示需要暂停执行的时间长度,单位是毫秒。 -
注意,
sleep
方法可能会抛出InterruptedException
异常,因此在使用时需要进行异常处理。
sleep()
方法通常用于 实现简单的时间延迟或限流操作,例如,在某些场景下需要将程序执行的速度降低,避免过快地占用系统资源,就可以使用sleep方法暂停线程执行。
sleep
方法并不是让当前线程绝对地休眠指定时间,实际暂停的时间可能会受到系统调度器的影响。
另外,如果在执行sleep期间,线程被中断,则会立刻抛出 InterruptedException
异常,并中断当前线程的休眠状态。因此,需要在使用sleep方法时注意异常处理和线程中断的情况。
join()
join()
方法将调用线程置于阻塞状态,直到被调用线程执行完毕后才会解除阻塞,继续执行调用线程的后续代码。
当一个线程调用另一个线程的 join()
方法时,调用线程会被阻塞,直到被调用的线程执行完毕或超时。
如果被调用的线程已经执行完毕,即处于终止状态,那么调用join()方法的线程将立即解除阻塞。
join()
方法的语法如下:
public final void join() throws InterruptedException
join()
方法还提供了一个重载版本,允许指定等待的最长时间:
public final synchronized void join(long millis) throws InterruptedException
-
millis
参数表示等待的时间长度,单位是毫秒。 -
如果被调用的线程在指定的时间内没有执行完毕,join()方法会自动解除阻塞,继续执行调用线程的后续代码。
-
join()
方法可能会抛出InterruptedException
异常,因此在使用时需要进行异常处理
join()
方法常用于 需要等待其他线程执行完毕后再执行 的场景,例如,主线程需要等待子线程执行完毕才能继续执行后续的操作。通过调用join()方法,可以实现线程之间的同步和协调。
join()
方法可以用于线程的串行执行,但不适合用于并行执行,因为在等待期间,调用线程会被阻塞,无法利用 CPU 资源执行其他任务。如果需要并行执行任务,可以考虑使用线程池等并发编程工具。
setDaemon()
setDaemon()
方法,用于设置线程的守护状态。
setDaemon()
方法的语法如下:
public final void setDaemon(boolean on)
on
参数表示需要设置的守护状态。
如果on
为true
,表示将线程设置为守护线程;
如果on
为false
,表示将线程设置为普通线程。
需要注意的是,setDaemon()方法只能在调用start()方法之前调用,否则会抛出 IllegalThreadStateException
异常。
如果一个线程被设置为守护线程,它在结束时并不会执行 finally块
中的代码,因此它不能执行一些必须清理资源的操作,例如关闭文件等。
在Java中,垃圾回收线程就是一个典型的守护线程。当所有的前台线程执行完毕后,垃圾回收线程负责清理不再使用的内存资源。
一般情况下,守护线程通常是在创建其他前台线程之前创建的,可以通过setDaemon()
方法将它们设置为守护线程,以便它们在所有前台线程结束后自动结束,不会继续占用系统资源。
需要注意的是,在使用 setDaemon()
方法时需要谨慎,因为它会影响线程的生命周期和行为,可能会导致一些无法预料的问题。使用守护线程时需要确保它们不会影响程序的正确性和安全性。
守护线程
守护线程的特点是:当所有的前台线程(非守护线程)都执行完毕后,JVM会自动退出,而不会管守护线程是否执行完毕。换句话说,守护线程的生命周期不影响整个程序的生命周期。
守护线程通常被用来在后台执行一些支持性任务,例如垃圾回收线程、定时任务线程等。它们不需要关注任务的完成与否,只需在程序运行期间提供某种支持功能即可。
如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。
线程状态
在 Java 中,线程有多种状态
操作系统中的线程状态
在操作系统中,线程被视为轻量级的进程,所以线程状态其实和进程状态是一致的。
操作系统的线程主要有以下三个状态:
-
就绪状态(ready)
:线程正在等待使用 CPU,经调度程序调用之后进入 running 状态。 -
执行状态(running)
:线程正在使用 CPU。 -
等待状态(waiting)
: 线程经过等待事件的调用或者正在等待其他资源(如 I/O)。
getState()
getState()
方法用于获取线程的状态
NEW
NEW
是线程的初始状态,表示线程对象已创建但尚未启动执行。此时线程还没有分配处理器资源,也没有开始执行线程的任务代码。
当使用 new
关键字创建一个线程对象时,线程处于 NEW
状态。此时可以对线程对象进行一些初始化操作,例如设置线程的名称、优先级等。但要注意,只有调用线程对象的 start()
方法后,线程才会开始执行,并进入到 RUNNABLE
状态。
在 NEW
状态下,无法多次启动同一个线程对象。如果尝试多次调用同一个线程对象的 start()
方法,会抛出 IllegalThreadStateException
异常。
一旦线程进入到 RUNNABLE
状态,它就可以被调度执行,并且可以切换到其他状态,例如 BLOCKED
、WAITING
、TIMED_WAITING
和 TERMINATED
等。
RUNNABLE
Thread.State
源码对 RUNNABLE
的定义
Java 线程的 RUNNABLE 状态可以理解为包括了操作系统线程的 ready 和 running 两个状态
RUNNABLE
状态表示线程正在执行或者准备执行。当调用线程对象的 start()
方法后,线程进入到 RUNNABLE
状态,此时系统会为线程分配资源,并且将线程的状态设置为 RUNNABLE
。线程对象会在等待 CPU 时间片的过程中不断轮询自己的任务,一旦拥有 CPU 时间片,就可以开始执行任务代码。
在 RUNNABLE
状态下,线程可以在 CPU 时间片内执行任务,也可以由于 I/O 等原因阻塞,此时线程状态会被切换到其他状态。
例如,如果线程请求获取一个[[初识锁机制|锁]],但是该锁已经被其他线程占用,那么线程就会被阻塞,进入到
BLOCKED
状态。另外,在RUNNABLE
状态下,线程也可以通过sleep()
等方法自愿地放弃 CPU 时间片,让出 CPU 资源给其他线程使用。
当线程完成任务并退出时,状态会从 RUNNABLE
转变为 TERMINATED
。
注意,线程的调度、切换和执行是由操作系统的线程调度器控制的,我们不能直接干预线程的切换。对于线程的处理,我们只能控制线程的行为,而不能直接控制线程的状态。
因此,合理设计线程的任务逻辑,减少阻塞和等待时间,可以提高线程的执行效率和性能。
BLOCKED
BLOCKED
是线程的阻塞状态,表示线程被阻塞而无法继续执行。当一个线程请求获取一个对象的[[初识锁机制|锁]]资源,而该锁资源已被其他线程占用时,该线程就会进入 BLOCKED
状态。
一个常见的场景是,当线程 A 持有某个对象的锁资源时,线程 B 也想要获取该对象的锁资源,但由于线程 A 尚未释放锁资源,线程 B 就会进入
BLOCKED
状态。在此状态下,线程 B 会被暂停执行,并等待直到获取到锁资源为止。当线程 A 释放了锁资源后,线程 B 会从
BLOCKED
状态中恢复,然后进入到RUNNABLE
状态,继续执行任务。
BLOCKED
状态是线程的一种暂时性状态,意味着线程正在等待某个事件发生。除了等待锁资源之外,线程也可能因为其他原因(如等待 I/O 操作完成)而进入 BLOCKED
状态。
在调试程序时,如果发现线程在 BLOCKED
状态下持续较长时间,并且并发性能下降,可能需要检查是否存在锁竞争或其他资源瓶颈问题。
WAITING
进入等待状态
WAITING
是线程的等待状态,表示线程等待某个特定条件的发生。当线程调用:
-
Object
类的wait()
调用
wait()
方法前线程必须持有对象的锁。线程调用
wait()
方法时,会释放当前的锁,直到有其他线程调用notify()
/notifyAll()
方法唤醒等待锁的线程。其他线程调用
notify()
方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()
方法的线程。同样,调用
notifyAll()
方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度 -
Thread
类的join()
一直等待这个线程执行完毕(转换为 TERMINATED 状态)
-
LockSupport
类的park()
线程会进入 WAITING
状态。
唤醒线程
在 WAITING
状态下,线程会被暂停执行,并等待其他线程发出唤醒信号,以便继续执行任务。唤醒信号可以由以下操作触发:
-
调用
Object
类的notify()
或notifyAll()
方法,来唤醒等待在同一对象上的线程。 -
被中断,即通过
interrupt()
方法来中断线程。 -
超时,即在调用
Object
类的wait(long timeout)
或Thread
类的join(long timeout)
方法时设定了超时时间。
一旦接收到唤醒信号或满足等待条件,线程就会从 WAITING
状态中恢复,并进入到 RUNNABLE
状态,继续执行任务。
WAITING 和 TIMED_WAITING
需要注意的是,与 WAITING
状态相似的状态还有 TIMED_WAITING
,两者之间的区别在于 TIMED_WAITING
状态具有超时等待的特性,而 WAITING
状态不具备。当线程处于等待状态时,如果设置了超时时间,那么状态会是 TIMED_WAITING
,否则为 WAITING
。
在编写多线程代码时,合理使用等待和唤醒机制可以避免线程的空转和资源浪费,提升线程的效率和响应性。
TIMED_WAITING
进入超时等待状态
TIMED_WAITING
是线程的超时等待状态,与 WAITING
状态类似,但具有超时等待的特性。
当线程调用:
-
Thread
类的sleep(long milliseconds)
:使当前线程睡眠指定时间,暂时使线程停止执行,并不会释放锁。 -
Object
类的wait(long timeout)
:线程休眠指定时间,等待期间可以通过notify()
或notifyAll()
唤醒。 -
Thread
类的join(long millis)
:等待当前线程最多执行 millis 毫秒,如果 millis 为 0,若调用的线程已经执行完毕,则当前线程会立即继续执行;若调用的线程尚未执行完毕,则当前线程会一直等待,直到调用的线程执行完毕。具体来说,使用
join(0)
主要有两种情况:-
当前线程需要等待其他线程的执行结果:在多线程环境中,有时候我们需要等待其他线程执行完毕,然后再继续当前线程的执行,以获取其他线程的执行结果或保持线程的有序性。这时,可以使用
join(0)
方法来等待指定线程执行完毕,保证后续操作在结果可用或有序执行的情况下进行。 -
当前线程需要等待某个条件满足后再继续执行:在多线程编程中,有时候我们需要等待某个条件满足后再进行后续操作,例如等待某个资源的释放或其他线程的通知。这时,可以使用
join(0)
方法来等待一个线程完成,然后在条件满足后再继续执行。
-
-
LockSupport
类的parkNanos(long nanos)
和
parkUntil(long deadline)
:除非获得调用许可,否则禁用当前线程进行线程调度指定时间
等方法时,线程会进入 TIMED_WAITING
状态。
唤醒线程
在 TIMED_WAITING
状态下,线程会被暂停执行,并等待一段指定的时间。一旦等待时间结束或满足其他条件,线程就会从 TIMED_WAITING
状态中恢复,并进入到 RUNNABLE
状态,继续执行任务。
与 WAITING
状态不同的是,TIMED_WAITING
状态具有超时等待的特性,可以设置等待的时间。在超时等待期间,线程会进行休眠,不会占用 CPU 资源。
需要注意的是,在调用超时等待方法时,等待时间可以是以毫秒(sleep
、wait
、join
)或纳秒(parkNanos
)为单位的。如果等待时间为零或负数,那么线程会立即恢复执行,不会进入等待状态。
合理使用超时等待可以使线程在等待某个条件满足或一段时间过去后自动恢复执行,从而提高代码的灵活性和响应性。
TREMINATED
TERMINATED
表示线程已经执行完毕,进入了结束状态。如果线程是因为 run()
方法完成正常退出,或因异常而非正常退出的,则会进入 TERMINATED
状态
当一个线程处于 TERMINATED
状态后,该线程将不会再次运行,该线程的所有资源都会被释放,包括栈、内存和 CPU 时间。
当一个线程的状态为 TERMINATED
状态时,可以使用 Thread.join()
方法等待该线程结束,或使用其他方法对其进行处理。
需要注意的是,虽然线程处于 TERMINATED
状态,但其占用的对象锁可能仍然没有释放,因此必要时需要注意避免死锁等问题,提高程序的稳定性和可靠性。
线程状态转换
线程的状态转换是根据线程的执行情况和调度器的调度来确定的,调度器可以根据算法和策略动态地改变线程的当前状态。
不同情况下,线程会从一种状态转换到另一种状态。例如,线程在运行状态下可能会被阻塞,或者等待状态下的线程可能被唤醒并返回到运行状态。
在多线程环境下,线程的状态转换是动态变化的。
以下是线程状态转换可能会出现的一些情况
-
新建状态(NEW) -> 运行状态(RUNNABLE)
:通过调用线程的start()
方法,线程从新建状态转换到运行状态。在运行状态中,线程可能正在执行,也可能在等待运行资源(如CPU)。 -
运行状态(RUNNABLE) -> 阻塞状态(BLOCKED)
:当线程试图获取某个对象的锁时,如果该锁被其他线程占用,线程将进入阻塞状态,等待锁被释放。 -
运行状态(RUNNABLE) -> 等待状态(WAITING)
:线程调用wait()
方法、join()
方法或LockSupport.park()
方法后,线程会进入等待状态,等待其他线程的通知、加入或唤醒。 -
运行状态(RUNNABLE) -> 计时等待状态(TIMED_WAITING)
:线程调用带有时间限制的等待方法,如wait(timeout)
、join(timeout)
或LockSupport.parkNanos(timeout)
、sleep(long millis)
方法后,线程会进入计时等待状态,等待指定的时间。 -
阻塞状态(BLOCKED) -> 运行状态(RUNNABLE)
:当线程获取到所需的锁后,会从阻塞状态转换回运行状态。 -
等待状态(WAITING)、计时等待状态(TIMED_WAITING) -> 运行状态(RUNNABLE)
:当其他线程调用等待线程所在对象的notify()
、notifyAll()
方法或LockSupport.unpark()
方法时,等待的线程将被唤醒,进入运行状态。 -
运行状态(RUNNABLE) -> 终止状态(TERMINATED)
:线程完成了run()
方法中的代码执行,正常结束或者因异常而非正常结束,线程将进入终止状态。
线程中断
在某些情况下,我们在线程启动后发现并不需要它继续执行下去时,需要中断线程。目前在 Java 里还没有安全方法来直接停止线程,但是 Java 提供了线程中断机制来处理需要中断线程的情况。
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。
中断线程
在 Java 中,可以通过调用线程的 interrupt()
方法来中断线程,这个方法会将线程的中断标志位设置为 true
。
但需要注意的是,该方法并不会强制中断线程,而是通知线程中断的状态,具体如何响应线程中断取决于线程本身,可以在合适的时机中断请求,也可以完全不处理继续执行下去。
当线程调用 interrupt()
方法时,有以下两种情况:
-
线程处于阻塞状态:如果线程处于阻塞状态(例如调用了
sleep()
、wait()
、join()
等方法),那么该线程会立即抛出InterruptedException
异常,并且将中断标志位重新设置为false
。 -
线程处于非阻塞状态:如果线程处于非阻塞状态,那么中断标志位会被置为
true
,但线程不会被立即中断。线程可以在适当的时候检查中断标志位,并根据标志位的状态来决定是否中断线程。
可以通过 Thread.interrupted()
方法来检查线程的中断状态并清除中断标志位。
需要注意的是,在处理线程中断时,要尽量避免阻塞操作和长时间循环等处理,以免影响线程的响应性能。同时,应该遵循“响应中断、恢复中断”的原则,在中断状态被清除前,尽可能避免阻塞操作,并尽快恢复中断状态。
判断线程中断
-
Thread.currentThread().isInterrupted()
:测试当前线程是否被中断。线程的中断状态会受这个方法的影响,调用一次可以使线程中断状态变为 true,调用两次会使这个线程的中断状态重新转为 false; -
Thread.isInterrupted()
:查询线程的中断状态,并根据需要来处理线程中断。不会影响线程中断状态
线程性能问题
由于多线程的 创建线程
和 线程上下文切换
的开销,多线程并发不一定比单线程串行执行快。
创建线程
『创建线程』是直接向系统申请资源的,对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度等。
线程上下文切换
CPU 是很宝贵的资源,速度非常快,通常 CPU 会给不同的线程分配 时间片
,当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行线程的本地数据,程序指针等,也就是『上下文切换』
减少上下文切换
-
协程(Coroutine):协程是一种轻量级的用户态线程,能够在在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换,而无需进行系统级的上下文切换。
协程可以利用异步编程模型来实现代码的流程控制,避免了线程切换所带来的额外开销。
-
线程池:合理使用线程池可以减少线程的创建和销毁,在任务之间进行复用,避免了频繁的上下文切换。
-
非阻塞式I/O:采用非阻塞式的I/O操作可以减少线程被挂起等待的时间,减少上下文切换的次数。可以利用异步I/O框架或者事件驱动的方式来实现。
-
调整线程优先级:合理设置线程的优先级,避免线程频繁抢占资源导致的上下文切换过多。
-
合理设计任务调度策略:根据实际需求,合理安排任务的执行顺序和调度策略,避免不必要的上下文切换。
-
减少锁竞争:在多线程环境中,锁的争用会导致线程频繁切换,可以采用无锁数据结构或更细粒度的锁来减少锁竞争情况,从而减少上下文切换。
-
使用最少线程:避免创建不必要的线程,如果任务很少,但创建了很多的线程,这样就会造成大量的线程都处于等待状态。
-
CAS 算法:利用
Atomic + CAS算法
来更新数据,采用乐观锁的方式,可以有效减少一部分不必要的锁竞争带来的上下文切换。
线程组
ThreadGroup
是一种线程组的概念,它可以将多个线程归类为同一个线程组,并且可以对线程组进行相关操作,例如设置异常处理器、设置线程优先级等。通过使用线程组,可以方便地管理多个线程,提高程序的可维护性和可读性。
每个 Thread
必然存在于一个 ThreadGroup
中,Thread
不能独立于 ThreadGroup
存在。执行main()
方法的线程名字是 main,如果在 new Thread 时没有显式指定,那么默认将父线程(当前执行 new Thread 的线程)线程组设置为自己的线程组。
线程安全性
线程组不是线程安全的,需要在多线程环境中进行同步操作。而且,一旦线程被加入到一个线程组中,就无法更改其所属的线程组。
线程组的数据结构
ThreadGroup
是一个标准的向下引用的树状结构,一个线程组可以包含多个线程和子线程组。这样设计可以防止"上级"线程被"下级"线程引用而无法有效地被 GC 回收。
线程组可以起到统一控制线程的优先级和检查线程权限的作用
常用方法
创建线程组
使用 ThreadGroup
类的构造函数来创建新的线程组,例如:
ThreadGroup group = new ThreadGroup("myThreadGroup");
将线程加入线程组
将线程加入线程组:使用 Thread
类的带有 ThreadGroup
参数的构造函数来创建一个新的线程,并将它加入线程组:
Thread thread1 = new Thread(group, runnable1);
Thread thread2 = new Thread(group, runnable2);
管理线程组
使用 ThreadGroup
类提供的方法来管理线程组,例如:
-
ThreadGroup.getName()
:获取线程组的名称。 -
ThreadGroup.activeCount()
:获取线程组中活动线程的数量。 -
ThreadGroup.enumerate(Thread[] list)
:将线程组中活动线程的引用存放到指定的数组中。 -
ThreadGroup.setMaxPriority(int newPriority)
:设置线程组中所有线程的最大优先级。 -
ThreadGroup.setDaemon(boolean daemon)
:设置线程组是否为守护线程。 -
ThreadGroup.uncaughtException(Thread t, Throwable e)
:设置线程组的异常处理器,用于处理线程组中的未捕获异常。
线程优先级
在Java多线程编程中,线程的优先级是指线程获取CPU资源的优先级。线程的优先级是一个整数,在默认情况下,线程的优先级为5,即中等优先级。
可以通过调用 Thread
类中的 setPriority(int priority)
方法来设置线程的优先级,优先级的值从 1~10
,数字越大表示优先级越高,高优先级的线程将会比低优先级的线程有更高的概率得到执行。
但并不是所有的操作系统都支持 10 级优先级的划分(比如有些操作系统只支持 3 级划分:低、中、高)
需要注意的是,线程优先级只是给操作系统一个建议,操作系统会尽量遵守这个建议,但并 不一定按照这个建议进行调度,真正的调用顺序由操作系统的线程调度算法决定,因此不能完全依赖线程优先级来控制程序的执行顺序。
如果线程优先级不恰当地设置得太高,可能会导致其他线程无法得到充分的CPU时间,从而导致其他线程无法正常执行。
设置优先级
可以使用 Thread
类的 getPriority()
方法来获取线程的优先级。另外,可以通过 Thread.MIN_PRIORITY
、Thread.NORM_PRIORITY
、Thread.MAX_PRIORITY
这三个常量来表示最小、中等和最大的优先级。
需要注意的是,在多线程编程中,除了线程优先级外,还有其他一些因素会影响线程的调度顺序,例如线程调度器的具体实现、CPU的负载状况等。
在设计多线程程序时,应该合理分配线程的优先级,同时运用其他控制线程顺序的技术和手段,来达到程序运行的合理、高效、可维护的目的。
线程调度器
Java 提供了一个线程调度器来监视和控制处于RUNNABLE 状态的线程。
-
线程的调度策略采用抢占式的方式,优先级高的线程会比优先级低的线程有更大的几率优先执行。
-
在优先级相同的情况下,会按照“先到先得”的原则执行。
主线程优先级
每个 Java 程序都有一个默认的主线程,就是通过 JVM 启动的第一个线程——main
线程。
在大多数情况下,主线程并不一定会优先调用。线程的调度顺序是由操作系统的线程调度器来决定的,调度器根据一系列的调度算法来确定应该运行哪个线程。
主线程只是在程序启动时创建的第一个线程,并且它通常是最后一个结束的线程,因为它负责执行程序的入口点,并且会等待其他线程执行完毕。但是,主线程和其他线程在调度上没有绝对的优先级差异。
在并发编程中,所有线程都会竞争处理器的资源,操作系统可能会根据线程的优先级、调度算法、以及当前系统的负载情况来决定当前应该运行哪个线程。因此,不能依赖主线程一定会优先调用。
如果需要确保主线程先执行,可以使用线程间的同步机制,例如 Thread.join()
方法,可以在其他线程中使用该方法来等待主线程执行完毕。
守护线程优先级
在 Java 中,守护线程(Daemon Thread)的优先级与普通线程的优先级没有直接关联。守护线程的优先级被默认设置为普通线程的优先级(通常为5或默认值)。
守护线程的优先级提高并不会影响其调度顺序或优先级权重。它们仍然处于与其他线程相同的调度队列中,并根据操作系统的调度策略和算法在运行和休眠之间切换。
虽然可以更改守护线程的优先级,但这通常不会对守护线程的行为和相对执行顺序产生直接影响。
线程组优先级的限制
如果某个线程的优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。