【多线程初阶】Thread类常见方法以及线程的状态

多线程初阶系列目录

持续更新中

1.第一次认识线程
2.Thread类常见方法以及线程的状态


文章目录

  • 多线程初阶系列目录
  • 前言
  • 1. Thread 类及常见方法
    • 1.1 常见构造方法
    • 1.2 常见属性
    • 1.3 重要方法
      • 1.3.1 启动一个线程 ---- start()
      • 1.3.2 中断一个线程 ---- interrupt()
      • 1.3.3 等待一个线程 ---- join()
      • 1.3.4 获取当前线程 ---- currentThread()
      • 1.3.5 休眠当前线程 ---- sleep()
  • 2. 线程的状态
    • 2.1 观察线程所有的状态
    • 2.2 线程状态和状态转移的意义
  • 总结


前言

本文是属于多线程初阶内容系列的, 如果还没有学习过之前文章的, 请先移步博主的之前的文章进行学习, 本文就是在学会线程的创建后, 再带大家认识一下 Thread 类以及其常见的方法, 再给大家讲解一下线程都有哪些状态.

关注收藏, 开始学习吧


1. Thread 类及常见方法

1.1 常见构造方法

通过我们上篇文章的学习, 我们已经学会了如何创建一个线程, 创建线程的方式主要有两种, 一种是继承 Thread 类, 一种是实现 Runnable 接口, 分别对应着下面, 第一和第二种构造方法, 而第三第四种构造方法, 是在第一第二种的基础上, 加了一个 name 参数, 效果是在创建线程对象时, 可以给其进行命名.

五种常见构造方法(第五种了解即可):

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("我的线程");
Thread t4 = new Thread(new MyRunnable(), "我的线程");

利用 jconsole 给大家简单演示一下取了别名的效果.

public class ThreadDemo6 {

    public static void main(String[] args) {

        Thread t = new Thread( () -> {
            while (true) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "我的线程");
        
        t.start();
    }

}

【多线程初阶】Thread类常见方法以及线程的状态_第1张图片
可以看到, 在线程中有一个名为 “我的线程” 的线程, 是由我们自己命名的.

1.2 常见属性

每个线程, 都有自己的名称, 状态, 优先级, 上下文, 记账信息等等 (之前在讲到进程中这些属性时, 其实都是线程的, 只不过之前谈到的进程是属于只有一个线程的进程).

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted

ID, 名称, 状态, 优先级都比较好理解, 我们重点讲一下后台进程, 存活, 被中断的这三个属性.

  • ID 是线程的唯一标识, 不同线程不会重复.
  • 名称是各种调试工具需要用到的.
  • 状态表示线程当前所处的一个情况, 下面我们会进一步说明.
  • 优先级高的线程理论上来说更容易被调度到.
  • isDaemon() 是否后台线程: true 表示的是后台线程, false 表示的是前台线程. 那么前台线程和后台线程有什么区别呢? 后台线程不会阻止 Java 进程结束, 哪怕后台线程还没有执行完, Java 进程该结束时就结束了. 而前台线程会阻止 Java 进程结束, 必须得 Java 进程中的所有前台线程都执行结束后, Java进程才能结束. 我们创建的线程默认是前台的. 可以通过 setDaemon() 方法来设置成后台.
  • isAlive() 是否存活: 描述系统内核里的那个线程, 是否存活. 怎样判定线程是否存活呢? 线程的入口方法执行完毕, 此时系统中对应的线程就没了, 调用该方法结果就是 false.
  • isInterrupted 是否被中断: 描述系统内核里的那个线程, 是否被中断. 线程的中断问题, 下面我们进一步说明, 在之后的重要方法 interrupt() 中会重点讲述, 这里我们知道即可.

给大家看一个程序演示一下这些属性:

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还活着");
                    Thread.sleep(1 * 1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
       });
        System.out.println(Thread.currentThread().getName() 
                           + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() 
                           + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() 
                           + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() 
                           + ": 优先级: " + thread.getPriority());
 		System.out.println(Thread.currentThread().getName() 
                           + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() 
                           + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() 
                           + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName() 
                           + ": 状态: " + thread.getState());
   }
}

【多线程初阶】Thread类常见方法以及线程的状态_第2张图片

1.3 重要方法

下面我们重点介绍一下 Thread 类中的几个重要方法.

1.3.1 启动一个线程 ---- start()

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象, 但线程对象被创建出来并不意味着线程就开始运行了.

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法, 就是喊一声: “行动起来!” , 线程才真正独立去执行了
    【多线程初阶】Thread类常见方法以及线程的状态_第3张图片

这里也有一个很重要的问题需要大家思考一下, 这是一道很经典的面试题, 请说明Thread类中run和start的区别:

这里, 我们从方法的作用功能, 及运行结果的区别分别说明.

  • 作用功能不同:
    • run 方法的作用是描述线程具体要执行的任务.
    • start 方法的作用是真正的去申请系统线程.
  • 运行结果不同:
    • run 方法是一个类中的普通方法, 主动调用和调用普通方法一样, 会顺序执行一次.
    • start 调用方法后, start 方法内部会调用 Java 本地方法(封装了对系统底层的调用)真正的启动线程, 并执行 run 方法中的代码, run 方法执行完成后线程进入销毁阶段.

调用 start 方法, 才是真的在操作系统的底层创建出一个线程.

1.3.2 中断一个线程 ---- interrupt()

中断一个线程, 这里就是字面意思, 就是让一个线程停下来, 也就是线程的终止. 本质上来说, 让一个线程终止, 办法只有一种, 就是让该线程的入口方法执行完毕.

给大家再用上面的例子讲一下:

李四一旦进到工作状态, 他就会按照行动指南上的步骤去进行工作, 不完成是不会结束的. 但有时我们需要增加一些机制, 例如老板突然来电话了, 说转账的对方是个骗子, 需要赶紧停止转账, 那张三该如何通知李四停止呢? 这就涉及到我们的停止线程的方式了.

目前常见的让线程的入口方法结束的方式有以下两种:

  1. 通过共享的标记来进行沟通, 也就是给线程中设置一个共享标记位.
  2. 调用 Thread 类中的 interrupt() 方法来通知.

示例1: 使用自定义的变量来作为标志位.

public class ThreadDemo7 {

    // 设置标志位
    public static boolean isQuit = false;

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!isQuit) {
                System.out.println("还没结束");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            System.out.println("我结束啦");
        });

        // 开启一个thread线程
        thread.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 3s 后, 在主线程中修改标志位
        isQuit = true;
    }

}

【多线程初阶】Thread类常见方法以及线程的状态_第4张图片
示例2: 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

上面我们是使用自己创建的变量来控制循环, 而 Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.

方法 说明
public void interrupt() 中断对象关联的线程, 如果线程正在阻塞, 则以异常方式通知, 否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置, 调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置, 调用后不清除标志位

使用 thread 对象的 interrupted() 方法通知线程结束.

public class ThreadDemo8 {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
        	// 两种方法均可以
            while (!Thread.interrupted()) {
//            while (Thread.currentThread().isInterrupted()) {
                System.out.println("还没结束");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 注意此处的 break
                    break;
                }
            }
            System.out.println("我结束啦");
        });

        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

		// 把 t 内部的标志位给设置成 true
        t.interrupt();
    }

}

thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起, 则以 InterruptedException 异常的形式通知, 清除中断标志.
    • 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以利用 break 跳出循环结束线程.
  2. 否则, 只是内部的一个中断标志被设置, Thread 可以通过
    • Thread.interrupted() 判断当前线程的中断标志被设置, 清除中断标志.
    • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置, 不清除中断标志.

这种方式通知收到的更及时, 即使线程正在 sleep 也可以马上收到.

示例3: 观察标志位是否清除

标志位是否清除, 就类似于一个开关.

  • Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”
  • Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为 “不清除标志位”.
  1. 使用 Thread.interrupted() , 线程中断会清除标志位.
public class ThreadDemo9 {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
            }
        }, "慧天城");

        t.start();

        t.interrupt();
    }

}

可以看到, 输出结果中, 只有一开始是 true, 剩下都是 false, 因为标志位被清除了.
【多线程初阶】Thread类常见方法以及线程的状态_第5张图片

  1. 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.
public class ThreadDemo10 {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().isInterrupted());
            }
        });

        t.start();
        t.interrupt();
    }

}

结果全部都是 true, 因为标志位没有被清除.
【多线程初阶】Thread类常见方法以及线程的状态_第6张图片

1.3.3 等待一个线程 ---- join()

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束, 最多等 millis 毫秒
public void join(long millis, int nanos) 同理, 但可以更高精度

线程之间是并发执行的, 操作系统对于线程的调度, 是无序的. 无法判断两个线程谁先执行结束, 谁后执行结束.

有时, 我们需要等待一个线程完成它的工作后, 才能进行自己的下一步工作.

看下面这段代码:

public class ThreadDemo11 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello t");
        });

        t.start();

        System.out.println("hello main");
    }
}

这段代码是先输出 hello main 还是 hello t 呢? 这是无法确定的, 虽然这个代码实际执行时, 大部分情况下都是先出 hello main, 但是也不能排除特定情况下, 主线程 hello main 还没有执行到, 先输出 hello t 的情况也会发生.

对于我们程序员来说, 这种情况是不能发生的, 因为我们需要有一个明确的执行顺序, 来确保程序不会出现 bug, 这时我们就可以使用线程等待来实现, 也就是 join() 方法.

public class ThreadDemo11 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello t");
        });

        t.start();

		// 使用join方法
        t.join();
        
        System.out.println("hello main");
    }

}

在 t.join() 执行时, 如果 t 线程还没有结束, main 线程就会阻塞等待, 代码都到这一行就会先停下来, 暂时不参与 CPU 的调度执行了.

在这里, 我们是在 main 线程中, 调用 t.join(), 意思就是让 main 线程先等待 t 线程结束, 再继续往下执行, 注意是只有 main 线程进入阻塞, 其余线程均不受影响.

补充t.join();

  1. main 线程在调用 t.join 的时候, 如果 t 线程还在运行, 此时, main 线程堵塞, 直到 t 执行完毕后( t 的 run 执行结束后), main 才从阻塞中解除, 才继续执行.
  2. main 线程调用 t.join 的时候, 如果 t 线程已经结束了, 此时 join 不会阻塞 main 线程, 就会立即往下执行.

但无论哪种情况, 都可以保证 t 线程是最先结束的那个

join 方法还有别的版本, 可以填写参数, 作为 “超时时间” (等待的最大时间).
【多线程初阶】Thread类常见方法以及线程的状态_第7张图片

  • join 的无参数版本, 效果就是 “死等”, 等待的那个进程什么时候结束, 就什么时候继续往下执行.
  • join 的有参数版本, 则是指定最大超过时间, 如果等待的时间到了上限, 还没等到, 就不等了.

1.3.4 获取当前线程 ---- currentThread()

这个方法, 我们应该就非常熟悉了, 这里也不过多讲解了.

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

1.3.5 休眠当前线程 ---- sleep()

这个也是我们比较熟悉一组方法, 有一点要记得, 因为线程的调度是不可控的, 所以, 这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的.

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throwsInterruptedException 可以更高精度的休眠
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }
}

2. 线程的状态

操作系统中的线程, 自身是有一个状态的, 在 Java Thread 中进行了对系统线程的进一步封装, 于是把这里的状态又进一步的精细化了.

2.1 观察线程所有的状态

线程的状态是一个枚举类型 Thread.State

public class ThreadState {
    public static void main(String[] args) {
    	// 查看线程所有的状态
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}

【多线程初阶】Thread类常见方法以及线程的状态_第8张图片

  • NEW: 安排了工作, 还未开始行动, 代表系统中的线程还没创建出来, 只是有了个 Thread 对象.
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 表示排队等着其他事情, 等待锁出现的状态.
  • WAITING: 表示排队等着其他事情, 使用 wait 方法出现的状态.
  • TIMED_WAITING: 表示排队等着其他事情, 指定时间等待 sleep 方法.
  • TERMINATED: 工作完成了, 系统中的线程执行完了, Thread 对象也还在.

2.2 线程状态和状态转移的意义

【多线程初阶】Thread类常见方法以及线程的状态_第9张图片
大家不要被这个状态转移图吓到, 我们重点是要理解状态的意义以及各个状态的具体意思.
【多线程初阶】Thread类常见方法以及线程的状态_第10张图片
还是我们之前的例子:

刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态.
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度.
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入 BLOCKEDWATINGTIMED_WAITING 状态. 这些状态有什么区别, 我们之后再进行了解. 如果李四、王五已经忙完,为 TERMINATED 状态.
所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的.


总结

✨ 本文主要讲解了 Thread 类及常见方法以及线程的状态是什么, 举了一些通俗的例子来帮助大家理解. 重点需要掌握 Thread 类中的一些重要方法, 以及 start 方法和 run 方法的区别, 还有线程都有哪些状态.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!

你可能感兴趣的:(多线程学习之路,java,开发语言,多线程,java-ee)