线程的概念
线程是操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。一个进程可以包含多个线程,每个线程是进程中一个单一顺序的控制流,并行执行不同的任务。
线程生命周期
线程的状态是指线程在执行过程中的不同阶段。以下是线程的几种常见状态介绍:
-
新建状态(New):当线程对象被创建但尚未启动时,线程处于这个状态。此时,线程已经有了相应的内存空间和其他资源,但还没有开始执行。
-
就绪状态(Runnable):线程已经被启动,并且准备好了运行,等待被线程调度器选中获取CPU的执行时间。在就绪状态的线程都等待在就绪队列中。
-
运行状态(Running):线程获得了CPU的执行时间,正在执行它的run()方法中的代码。线程调度器根据线程优先级和策略从就绪队列中选取线程进入运行状态。
-
阻塞状态(Blocked):线程因为某些原因暂时停止运行。例如,线程等待某个锁的释放,或者线程等待某些I/O操作完成。
-
等待状态(Waiting):线程等待其他线程执行特定操作(如通知)时进入此状态。线程在等待状态时不会被分配CPU执行时间。
-
超时等待状态(Timed Waiting):线程在一定时间内等待另一个线程的通知或某个特定操作完成。比如线程执行了sleep(1000)方法,如果超过了指定时间,线程会自动返回到就绪状态。
-
终止状态(Terminated):线程的run()方法执行完成后,线程进入终止状态。此时,线程已经完成了它的生命周期,并且不再参与线程调度。
了解线程的这些状态对于多线程编程中的调试和性能优化是非常重要的。
线程的等待、阻塞或者挂起都是指线程无法执行的状态,只不过等待更强调线程主动等待,而阻塞则更强调线程无法获取资源被动地等待,不用太纠结它们的区别,一般都统称为阻塞状态。
线程的基本操作
线程的创建
- 继承Thread类:新建一个类继承Thread类,重写它的run()方法。
public class ThreadA extends Thread{
@Override
public void run() {
System.out.println("这里编写线程要做的事情");
}
public static void main(String[] args) {
//1.新建实例并启动一个线程
new ThreadA().start();
//2.若无需复用也可以用匿名内部类实现
new Thread(() -> {
System.out.println("这里编写线程要做的事情");
}).start();
}
}
- 实现Runnable接口:新建一个类实现Runnable接口,实现它的run()方法。
public class ThreadB implements Runnable{
@Override
public void run() {
System.out.println("这里编写线程要做的事情");
}
public static void main(String[] args) {
new Thread(new ThreadB()).start();
}
}
- 实现Callable接口:这种方式可以获取线程执行的返回值。
public class ThreadC implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("这里编写线程要做的事情");
return 10;//返回结果
}
public static void main(String[] args) throws Exception {
ThreadC threadC = new ThreadC();
FutureTask<Integer> task = new FutureTask<>(threadC);
new Thread(task).start();
System.out.println(task.get());//获取call()的返回结果
}
}
创建线程的方式还有使用线程池,只不过线程池内部也是使用new Thread()方式创建线程的。其实这些所谓的线程都只是Java层面的,new Thread()只是创建了一个Java对象而已,真正操作系统层面的线程是调用start()方法之后才创建的。
线程的终止
一般来说,线程的run方法执行完毕就会自动终止,无须手工关闭。但不是所有线程都可以执行完毕的。比如一些服务端的后台线程可能会常驻系统,它们通常不会正常终结。或许它们的执行体本身就是一个死循环,用于提供某些服务。Thread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程终止,非常方便。但是这是一个废弃的方法,非常不推荐使用。原因是stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
一种简单常见的方式是使用退出标志,在需要退出时通过别的线程将标志位exit设置成true,就能等run()方法执行完毕后自动退出,其中volatile关键字保证了其它线程对exit的改动是可见的。
public class ThreadService extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//处理业务
}
}
}
线程中断
线程中断是一种重要的线程协作机制,Thread提供了3个相关的方法:
- interrupt()中断线程
- isInterrupted()判断线程是否被中断
- interrupted()判断线程是否被中断并清楚中断状态
interrupt()的作用是设置线程的中断标志位,中断标志位可以中断阻塞状态,例如sleep()、wait()或join等原因导致的线程阻塞,使其立即抛出中断异常,这就是为什么sleep()和wait()会声明一个受检异常InterruptedException,抛出异常后中断标志位会被清除。线程也可以利用isInterrupted()方法判断自己是否被中断,从而选择是否要退出。利用中断机制可以实现线程的优雅终止。
public class ThreadService extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
其它方法
wait()方法
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到:
- 其他线程调用了该共享对象的notify()或者notifyAll()方法;
- 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
- 等待超时
- 虚假唤醒,notify()方法可能会唤醒多个线程(理论上,实际不常发生)。
调用wait()方法会释放锁,既然要释放锁那必须得先拥有锁,所以wait()方法须在synchronized同步块中调用。
String a = "123";//共享变量
boolean waitFlag = true;//挂起判断条件
//先获取共享变量的锁
synchronized (a){
while (waitFlag){//判断挂起条件,防止虚假唤醒
a.wait();
}
}
notify()和notifyAll()
notify()随机唤醒在此对象监视器上等待的单个线程,notifyAll()唤醒在此对象监视器上等待的所有线程。
join()
join() 在当前线程中调用A线程的 join() 方法,则当前线程转为阻塞状态,等待A线程执行完成。很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。实际工作中CountDownLatch是比join()更好的选择。
sleep()
使线程阻塞挂起指定的时间,但不会释放锁。
yield()
线程让出自己剩余的时间片,转入就绪状态。
守护线程
守护线程是一种特殊的线程,常用于后台完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。可以调用线程的setDaemon(true)方法将线程设置成守护线程。
参考
《Java高并发程序设计》