一、多线程的基本原理

线程的start方法,实际上底层做了很多事情,具体的实现简图如下,画得不一定工整,但是能够表达大概意思就行。

OS调度算法有很多,比如先来先服务调度算法(FIFO)、最短优先(就是对短作业的优先调度)、时间片轮转调度等

线程的运行状态

先来了解线程的运行状态,

public class ThreadStatus {
    public static void main(String[] args) {
        //TIME_WAITING
        new Thread(()->{
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(100);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        ,"timewaiting").start();
        //WAITING,线程在ThreadStatus类锁上通过wait进行等待
        new Thread(()->{
            while(true){
                synchronized (ThreadStatus.class){
                    try {
                        ThreadStatus.class.wait();
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        ,"Waiting").start();
        //线程在ThreadStatus加锁后,不会释放锁
        new Thread(new BlockedDemo(),"BlockDemo-01").start();
        new Thread(new BlockedDemo(),"BlockDemo-02").start();
    }
    static class BlockedDemo extends Thread{
        public void run(){
            synchronized (BlockedDemo.class){
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
运行上述示例,打开终端命令,输入"jps"(显示当前所有Java进程pid);根据获取到的pid, 通过 jstack pid ,可以打印指定Java进程ID的堆栈信息通过堆栈信息,可以看到线程的运行状态

线程的状态

通过上面这段代码可以看到,线程在运行过程中,会存在几种不同的状态,一般来说,在Java中,线程的状态一共是6种状态,分别是

  • NEW:初始状态,线程被构建,但是还没有调用start方法
  • RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
  • BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况

    • Ø 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
    • Ø 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中
    • Ø 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复
  • WAITING: 等待状态
  • TIME_WAITING:超时等待状态,超时以后自动返回
  • TERMINATED:终止状态,表示当前线程执行完毕

二、线程的终止

如何正确停止一个线程呢?这个问题要细聊,还是有很多东西可以说的。

我们知道Thread提供了线程的一些操作方法,比如stop、suspend等,这些方法可以终止一个线程或者挂起一个线程,但是这些方法都不建议大家使用。原因比较简单,

举个例子,假设一个线程中,有多个任务在执行,此时,如果调用stop方法去强行中断,那么这个时候相当于是发送一个指令告诉操作系统把这个线程结束掉,但是操作系统的这个结束动作完成不代表线程中的任务执行完成,很可能出现线程的任务执行了一般被强制中断,最终导致数据产生问题。这种行为类似于在linux系统中执行 kill -9类似,它是一种不安全的操作。

那么除了这种方法之外,还有什么方式可以实现线程的终止呢?要了解这个问题,我们首先需要知道,一个线程什么情况下算是终止了。

一个线程在什么情况下是执行结束了

我们分析一下下面这段代码,通过start()启动一个线程之后,本质上就是执行这个线程的run方法。那么如果这个线程在run方法执行完之前,一直处于运行状态,直到run方法中的指令执行完毕,那么这个线程就会被销毁

public class MyThread extends Thread {
    public void run() {
        System.out.println("MyThread.run()");
    }
}
MyThread myThread1 = new MyThread();
myThread1.start();

在正常情况下,这个线程是不需要人为干预去结束的。如果要强制结束,只能走stop这个方法。

那在哪些情况下,线程的中断需要外部干预呢?

  • 线程中存在无限循环执行,比如while(true)循环
  • 线程中存在一些阻塞的操作,比如sleep、wait、join等。

存在循环的线程

假设存在如下场景,在run方法中,存在一个while循环,因为这个循环的存在使得这个run方法一直无法运行结束,这种情况下,如何终止呢?

public class MyThread extends Thread {
    public void run() {
        while(true){
            System.out.println("MyThread.run()");
        }
    }
}
MyThread myThread1 = new MyThread();
myThread1.start();

按照我们开发的思维来说,首先要解决的就是,while(true)这个循环,必须要有一个结束条件,其次是 要在其他地方能够修改这个结束条件让该线程感知到变化。假设我们把while(true)改成while(flag),这个flag可以作为共享变量被外部修改,修改之后使得循环条件无法被满足,从而退出循环并且结束线程。

这段逻辑其实非常简单,其实就是给了线程一个退出的条件,如果没有这个条件,那么线程将会一直运行。

实际上,在Java提供了一个 interrupt方法,这个方法就是实现线程中断操作的,它的作用和上面讲的 这个案例的作用一样。

interrupt方法

当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。

线程通过检查自身是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。

public class InterruptDemo {
    private static int i;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                //默认情况下 isInterrupted返回false、通过thread.interrupt变成了true
                i++;
            }
            System.out.println("Num:"+i);
        }
        ,"interruptDemo");
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        //加和不加的效果
    }
}

这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅

处于阻塞状态下的线程中断

另外一种情况,就是当线程处于阻塞状态下时,我想要中断这个线程,那怎么做呢?

public class InterruptDemo {
    private static int i;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                try {
                    TimeUnit.SECONDS.sleep(1);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Num:"+i);
        }
        ,"interruptDemo");
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
    }
}

从这个例子中反馈出一个问题,我们平时在线程中使用的sleep、 wait、join等操作,它都会抛出一个 InterruptedException 异常,为什么会抛出异常,是因为它在阻塞期间,必须要能够响应被其他线程发 起中断请求之后的一个响应,而这个响应是通过InterruptedException来体现的。

但是这里需要注意的是,在这个异常中如果不做任何处理的话,我们是无法去中断线程的,因为当前的 异常只是响应了外部对于这个线程的中断命令,同时,线程的中断状态也会复位,如果需要中断,则还 需要在catch中添加下面的代码

public class InterruptDemo {
    private static int i;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                try {
                    TimeUnit.SECONDS.sleep(1);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                    //再次中断
                }
            }
            System.out.println("Num:"+i);
        }
        ,"interruptDemo");
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        System.out.println(thread.isInterrupted());
    }
}

所以,InterruptedException 异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如

  1. 直接捕获异常不做任何处理
  2. 将异常往外抛出
  3. 停止当前线程,并打印异常信息
最后修改:2022 年 06 月 23 日
如果觉得我的文章对你有用,请点个赞吧~