线程基础
Java 线程和操作系统的线程有啥区别?
JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下:
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。
创建线程的三种方式
- 继承
Thread类,重写run方法,start调用。- 优点:编写简单,使用 this 就可以获得当前线程。
- 缺点:Java 单继承使得这个类不能继承其他类。
- 实现
Runnable接口,重写run方法,start调用。 - 实现
Callable接口,重写call方法,通过FutureTask传参,start调用。- 优点:可以继承其他类,编写复杂的线程类。
- 缺点:编程复杂,使用
Thread.currentThread()获得当前线程。
一般来说,创建线程有很多种方式,例如继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池、使用 CompletableFuture 类等等。
不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。严格来说,Java 就只有一种方式可以创建线程,那就是通过 new Thread().start() 创建。不管是哪种方式,最终还是依赖于 new Thread().start()。
关于这个问题的详细分析可以查看这篇文章:大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!。
Runnable 和 Callable 的区别
- Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。
- Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。
- Call 方法可以抛出异常,run 方法不可以。
- 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
说说线程的生命周期和状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
- NEW: 初始状态,线程被创建出来但没有被调用
start()。 - RUNNABLE: 运行状态,线程被调用了
start()等待运行的状态。 - BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):

JVM 没有区分就绪和运行状态,原因在于操作系统一般使用时间分片的抢占式调度,每个时间片非常小,大概 0.01 秒的数量级,那么此时区分这两种状态意义不大。
sleep() 和 wait()
共同点:
- 暂停线程的执行。
不同点:
- 作用:
wait()通常被用于线程间同步/通信,sleep()通常被用于暂停执行。 - 方法:
wait()方法属于 Object 本地方法,因为是等待某个对象的锁。sleep()方法属于 Thread 静态本地方法,让当前线程暂停执行,不涉及到对象类。 - 苏醒:无参
wait()被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。有参的wait()超时后线程会自动苏醒。sleep()方法执行完成后,线程会自动苏醒。 - 锁:
sleep()方法没有释放锁,而wait()方法释放了锁。
下面的程序中,
- 线程 1 先执行、wait 进入等待并释放 MultiThread.class 锁。
- 线程 2 执行后,由于 MultiThread.class 锁被线程 1 调用 wait 方法释放了,因此占有该锁。之后调用 notify 方法,唤醒线程 1.
- 线程 2 等待 10 毫秒后,结束运行,释放锁。
- 线程 1 获得锁,继续刚刚的运行,最后结束,释放锁。
public class MultiThread {
private static class Thread1 implements Runnable{
public void run() {
synchronized(MultiThread.class){
try{
MultiThread.class.wait(); // 释放锁
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("thread1 is being over!");
}
}
}
private static class Thread2 implements Runnable{
public void run() {
synchronized(MultiThread.class){
MultiThread.class.notify();
try{
Thread.sleep(10); // sleep 不释放锁
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("thread2 is being over!");
}
}
}
public static void main(String[] args) {
new Thread(new Thread1()).start();
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
new Thread(new Thread2()).start();
}
}示例:Java中sleep()与wait()区别_行者小朱的博客-CSDN博客_java中wait和sleep的区别
start() 与 run()
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
start() 方法是创建一个新线程,新线程执行内部逻辑。run() 方法是当前线程去执行。
Thread 类的 yield 方法作用
yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield() 的线程有可能在进入到暂停状态后马上又被执行。
什么是上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
- 主动让出 CPU,比如调用了
sleep(),wait()等。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
守护线程是什么?
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
虚拟线程是什么?
虚拟线程在 Java 21 正式发布,这是一项重量级的更新。我写了一篇文章来总结虚拟线程常见的问题:虚拟线程常见问题总结,包含下面这些问题:
- 什么是虚拟线程?
- 虚拟线程和平台线程有什么关系?
- 虚拟线程有什么优点和缺点?
- 如何创建虚拟线程?
- 虚拟线程的底层原理是什么?