一. Thread类简介
Thread类是Java语言中用于实现多线程的核心类,它位于java.lang包中。每个Java程序至少有一个线程,即主线程,它是由Java虚拟机(JVM)自动创建的。我们也可以通过创建Thread类的实例来创建其他线程。
Thread类的主要作用包括:
- 创建新线程。
- 启动线程执行任务。
- 管理线程的状态和行为。
- 协调多个线程的执行。
下面是Thread类的一些主要方法:
- start(): 启动线程,使线程进入就绪状态,等待CPU调度。
- run(): 定义线程的任务逻辑,通常在子类中重写。
- sleep(long millis): 使当前线程睡眠指定的毫秒数。
- yield(): 使当前线程让出CPU,但仍处于就绪状态。
- join(): 等待线程执行完毕。
- interrupt(): 中断线程。
- isAlive(): 判断线程是否还活着。
除此之外,Thread类还提供了一些管理线程优先级、守护状态、线程组等的方法。
1.1 Thread类的生命周期
下图展示了Thread类的生命周期,以及各种方法对线程状态的影响:
1.2 JVM线程和本地线程
在JVM中,每个Java线程都会映射到一个本地线程上。JVM通过本地线程来执行Java线程的任务,并维护着它们之间的映射关系。
Java线程的优先级会映射到本地线程的优先级上,但操作系统可能会忽略这个建议,根据自己的调度策略来决定线程的执行顺序。
线程切换的过程包括:保存当前线程的上下文,恢复目标线程的上下文,更新JVM的线程映射关系等。这个过程需要一定的开销,因此频繁的线程切换会影响性能。
1.3 操作系统对线程的支持
在操作系统层面,线程是由线程调度器管理的。线程调度器决定了哪些线程应该运行,哪些线程应该等待,以及线程的执行顺序。
常见的线程调度算法包括:先来先服务(FCFS)、最短作业优先(SJF)、优先级调度、时间片轮转(RR)等。现代操作系统通常使用更复杂的调度算法,如多级反馈队列(Multilevel Feedback Queue)。
操作系统还提供了一些线程同步的原语,如互斥量(mutex)、信号量(semaphore)、条件变量(condition variable)等,用于协调多个线程对共享资源的访问。
二. 创建和启动线程
2.1 线程的创建和切换
在Java代码中,我们通常使用以下方式创建一个线程:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程任务
}
});
thread.start();
这段代码主要做了以下几件事:
- 创建一个实现了Runnable接口的匿名类实例,作为线程的任务。
- 创建一个Thread实例,将Runnable实例传递给它的构造函数。
- 调用Thread实例的start()方法,启动线程。
当调用start()方法时,JVM会创建一个新的线程来执行run()方法中的任务。注意,start()方法会立即返回,而不会等待线程执行完毕。
当调用Thread的构造函数时,JVM会执行以下操作:
- 在堆中为Thread对象分配内存。
- 初始化Thread对象的属性,如名称、优先级、守护状态等。
- 将传递给构造函数的Runnable对象关联到Thread对象上。
- 将Thread对象的状态设置为NEW。
当调用Thread的start()方法时,JVM会执行以下操作:
- 检查线程的状态,如果不是NEW状态,则抛出IllegalThreadStateException异常。
- 创建一个新的本地线程(如果线程组没有被销毁的话)。
- 将Java线程对象关联到本地线程上。
- 将线程的状态设置为RUNNABLE。
- 调用本地线程的start()方法,启动线程。
JVM使用一个线程管理器来管理所有的线程,包括主线程和用户创建的线程。线程管理器负责线程的创建、销毁、状态转换、调度等工作。
JVM还维护着一个线程映射表,用于将Java线程对象映射到本地线程上。当Java线程需要执行时,JVM会根据映射表找到对应的本地线程,然后由操作系统调度本地线程。
Thread的start()方法只能被调用一次,多次调用会抛出IllegalThreadStateException异常。这是因为一个线程只能被启动一次,重复启动没有意义,而且可能会导致不可预期的行为。
当调用start()方法时,线程的状态会从NEW变为RUNNABLE,然后等待JVM调度执行。如果再次调用start()方法,线程已经不在NEW状态了,因此会抛出异常。
如果你需要重复执行一个线程的任务,可以考虑在任务执行完后重新创建一个线程,或者使用线程池来管理线程的生命周期。
2.2 线程切换
在多线程环境下,线程切换通常会涉及用户态和内核态的转换。具体来说,线程切换可能会触发以下几种用户态和内核态的转换,涉及上下文的切换:
- 用户态到内核态 当线程因为以下原因而被暂停执行时,会从用户态切换到内核态:
- 系统调用:当线程调用了一个系统调用,如读写文件、申请内存等,会进入内核态,由操作系统处理该请求。
- 中断:当外部设备如键盘、鼠标等产生中断时,会进入内核态,由操作系统的中断处理程序处理该中断。
- 异常:当线程执行过程中发生异常,如除以0、缺页等,会进入内核态,由操作系统的异常处理程序处理该异常。
- 内核态到用户态 当操作系统完成了相应的处理后,会从内核态切换回用户态,继续执行线程的代码。这可能发生在以下情况:
- 系统调用返回:当操作系统处理完一个系统调用后,会将结果返回给线程,并切换回用户态。
- 中断处理完毕:当操作系统的中断处理程序处理完一个中断后,会切换回被中断的线程,继续执行。
- 异常处理完毕:当操作系统的异常处理程序处理完一个异常后,会切换回发生异常的线程,继续执行。
- 上下文切换
上下文切换开销具体涉及到保存和恢复以下信息:
- 程序计数器(PC) :需要保存当前程序执行的位置,以便在中断后能回到正确的执行点。
- 寄存器文件:包括CPU中所有的寄存器状态,这些寄存器保存着当前执行进程的操作参数和中间结果。
- 主存内容:必要时,需要保存进程的主存状态,特别是当涉及到复杂的内存管理技术时,比如保存当前进程的地址空间。
- 程序状态字寄存器(PSW) :反映了程序的当前状态,如条件码位,使得CPU知道上一次操作的结果是正数、零还是负数。
- 栈指针:如果进程在用户态有自己的栈,上下文切换时需要保存和恢复栈的状态。
- 内核模式下的栈:对于需要进入内核态执行的操作,如系统调用,还要涉及到内核栈的保存与恢复。
- 控制寄存器:在一些架构中,控制寄存器包含着CPU的重要控制位,它们需要在上下文切换时被保存和恢复。
- I/O状态:如果进程在执行I/O操作,那么相关的I/O状态和缓冲区信息也需要被保存。
在进行上下文切换时,操作系统会首先将这些信息保存到当前进程的进程控制块(PCB)中。PCB是操作系统用来描述进程状态的数据结构。随后,操作系统会加载新的进程的上下文信息,使其从上次停止的地方开始继续执行。
三. 管理线程的状态和行为
Thread类提供了一些方法来管理线程的状态和行为,如sleep()、join()、interrupt()等。
3.1 sleep()
sleep()方法可以使当前线程暂停执行一段时间,进入TIMED_WAITING状态。例如:
public class SleepExample {
public static void main(String[] args) {
new Thread(() -> {
try {
System.out.println("Thread starts sleeping");
Thread.sleep(2000);
System.out.println("Thread wakes up");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
在这个例子中,我们创建了一个匿名线程,在run()方法中调用Thread.sleep(2000),使线程休眠2秒钟。注意,sleep()方法可能抛出InterruptedException异常,我们需要捕获并处理它。
sleep()方法不会释放线程持有的锁,因此可能会影响其他线程的执行。
3.2 join()
join()方法可以等待线程执行完毕。如果一个线程A调用了另一个线程B的join()方法,那么线程A会进入WAITING状态,直到线程B执行完毕。例如:
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("Thread starts working");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread finishes working");
});
thread.start();
thread.join();
System.out.println("Main thread continues");
}
}
在这个例子中,我们在main()方法中创建并启动了一个线程,然后立即调用thread.join()方法。这会使主线程进入WAITING状态,等待子线程执行完毕后再继续执行。
3.3 interrupt()
interrupt()方法可以中断线程。如果一个线程A调用了另一个线程B的interrupt()方法,那么线程B的中断标志位会被设置。如果线程B处于WAITING、TIMED_WAITING或BLOCKED状态,则会抛出InterruptedException异常,并清除中断标志位。例如:
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread is working");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread is interrupted");
Thread.currentThread().interrupt();
}
}
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
在这个例子中,我们创建了一个线程,它在一个循环中工作,每次迭代都会检查自己的中断标志位。在主线程中,我们在3秒后调用thread.interrupt()方法中断子线程。
子线程在sleep()时被中断,会抛出InterruptedException异常。在异常处理中,我们再次设置中断标志位,以便在下一次循环时退出。
四. 最佳实践和注意事项
在使用Thread类时,有一些最佳实践和注意事项需要牢记:
- 优先使用实现Runnable接口的方式创建线程,而不是继承Thread类。
- 不要直接调用线程的run()方法,而应该调用start()方法。
- 对于长时间运行的线程,要有合适的中断机制。
- 避免对临界资源的不必要同步。
- 给线程起一个有意义的名字,方便调试和定位问题。
- 对于需要并发访问的共享变量,要使用适当的同步机制来保护。
- 避免在线程中使用可能抛出unchecked exception的过时方法。
- 在线程中捕获并处理InterruptedException异常,恢复线程的中断状态。
- 对于需要等待其他线程的情况,优先使用join()方法,而不是busy waiting。
- 在线程池中使用线程,而不是为每个任务创建一个新线程。