• Thread类的基本操作(JAVA多线程)


    目录

    线程的创建(Thread类)

    Thread类的一些常见构造方法

    Thread类中的一些常见属性:

    getId():

    isDaemon()

    isAlive()

    打断线程

    第一种方法:

    第二种方法:

    线程等待

    join()

    join(等待时间)

    休眠线程

    线程的状态


    线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并提供了一些API供外部使用。

    JAVA中 Thread类 将系统提供的API又近一步进行了抽象和封装,所以如果想要使用多线程就离不开 Thread 这个类。

    线程的创建(Thread类)

    在JAVA中 创建线程可以有多种方法,这里简单介绍几种。

    方法一:我们自己编写一个类使这个类继承自Thread类,然后重写里面的 run() 方法。

    1. class MeThread extends Thread {
    2. //必须要实现这个方法,此方法是新线程执行的入口方法(告诉线程应该做什么)
    3. @Override
    4. public void run() {
    5. System.out.println("这是新线程执行的任务");
    6. }
    7. }
    8. public static void main(String[] args) {
    9. //创建一个线程对象
    10. MeThread t = new MeThread();
    11. //调用系统API启动线程
    12. t.start();
    13. }

    当我们运行程序之后就会执行 run() 方法中的打印操作。

    new 一个Thread类只是创建出了一个线程,并不会调用系统API创建线程,只有当调用了start() 方法之后才会调用系统API在系统中创建出线程并启动。(此时不理解可以,因为在介绍isAlive()方法时会验证)

    这个重写的 run() 方法可以理解为一个任务,这个方法会在线程启动时自动被调用执行当线程执行完这个方法中的内容时该线程就会被销毁并且无法再次使用start()方法唤醒。

    此时我们为了可以更好的呈现多线程并发编程的效果对上述代码进行了一些细微修改。

    1. class MeThread extends Thread {
    2. //必须要重写这个方法,此方法是新线程执行的入口(告诉线程应该做什么)
    3. @Override
    4. public void run() {
    5. //此处让这个新线程每隔0.1s执行一次打印操作
    6. while (true) {
    7. try {
    8. Thread.sleep(100);
    9. } catch (InterruptedException e) {
    10. throw new RuntimeException(e);
    11. }
    12. System.out.println("这是创建的新线程执行的任务");
    13. }
    14. }
    15. }
    16. public static void main(String[] args) {
    17. MeThread t = new MeThread();
    18. //调用系统API启动线程
    19. t.start();
    20. //让主线程每隔0.1s执行一次打印操作
    21. while (true) {
    22. try {
    23. Thread.sleep(100);
    24. } catch (InterruptedException e) {
    25. throw new RuntimeException(e);
    26. }
    27. System.out.println("主线程");
    28. }
    29. }

    5420a28a4bc04b979e145e656e12fa9c.png

    此时可以看到两个while循环是“同时执行的” ,每个线程都是一个独立的执行流。

    代码执行之后可以看到程序在无规律的进行打印,其主要原因是系统对线程的调度是随机的。 


    方法二:我们自己编写一个类使实现 Runnable接口 然后重写里面的 run() 方法。

    • Thread类 实现了 Runnable接口;
    • Thread类 中的 run() 方法也是重写的 Runnable接口中的;
    • 因为在 Thread类 中提供了一个这样的构造方法:57fd3e2dbd40417aa31283ca59e7d382.png

    1. class MeRunnable implements Runnable{
    2. //必须要重写这个方法,此方法是新线程执行的入口
    3. @Override
    4. public void run() {
    5. System.out.println("这是一个新线程执行的任务");
    6. }
    7. }
    8. public static void main(String[] args) {
    9. MeRunnable runnable = new MeRunnable();
    10. Thread t = new Thread(runnable);
    11. //调用系统API启动线程
    12. t.start();
    13. }

    使用 Runnable 接口和直接继承 Thread 的区别就是:可以帮我们降低代码的耦合性也就是“解耦合”

    Runnable 它表示一个可以执行的任务 而它并不关心这个任务是啥,在哪里执行;这个任务也不一定和线程强相关,因为这个代码可能使用单线程、多线程还是不使用线程或者是用其他方法(例:线程池,协程……)执行都没有任何区别。

    而此时使用 Runnable 就可以将这个任务单独的提取出来,这样就可以随时改变这个任务是使用什么方法进行执行(例如:后面如果不想用线程了就可以直接在main方法中进行调用)例:

    1. class MeRunnable implements Runnable{
    2. @Override
    3. public void run() {
    4. System.out.println("这是一个任务");
    5. }
    6. }
    7. public static void main(String[] args) {
    8. MeRunnable runnable = new MeRunnable();
    9. //此时不想使用线程执行这个任务
    10. runnable.run();
    11. }

    方法三:使用匿名内部类。

    1. public static void main(String[] args) {
    2. Thread t = new Thread(){
    3. @Override
    4. public void run() {
    5. System.out.println("这是创建的新线程执行的任务");
    6. }
    7. };
    8. //调用系统API启动线程
    9. t.start();
    10. }

    因为在 Thread类 中提供了一个这样的构造方法:57fd3e2dbd40417aa31283ca59e7d382.png

    所以我们可以写成过这样: 

    1. public static void main(String[] args) {
    2. Thread t = new Thread(new Runnable(){
    3. @Override
    4. public void run() {
    5. System.out.println("这是一个新线程执行的任务");
    6. }
    7. });
    8. //调用系统API启动线程
    9. t.start();
    10. }

     方法四:因为Runnable接口是一个函数式接口,所以可以利用 Lambda表达式 来创建线程。

    1. public static void main(String[] args) {
    2. Thread t = new Thread(() -> System.out.println("这是创建的新线程执行的任务"));
    3. //调用系统API启动线程
    4. t.start();
    5. }

    除了观看控制台的输出结果来观察多线程之外,还可以使用JDK中带有的工具 jconsole 来更形象的观测:具体方法可以跳转这里icon-default.png?t=N7T8https://blog.csdn.net/2302_76339343/article/details/133760752

    Thread类的一些常见构造方法

    方法说明
    Thread()创建线程对象
    Thread(Runnable target)使用 Runnable 对象创建线程对象
    Thread(String name)创建线程对象,并命名
    Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
    Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组

    Thread类中的一些常见属性:

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

    getId():

    获取当前线程的 id 

    id 是线程的唯一身份标识,这个 id 是 JAVA 为这个线程分配的,并不是系统 API 分配的 ID ,更不是 PCB 的 ID 。

    1. public static void main(String[] args) {
    2. Thread t = new Thread(() -> {while (true) {
    3. try {
    4. Thread.sleep(100);
    5. } catch (InterruptedException e) {
    6. //打印异常
    7. e.printStackTrace();
    8. }
    9. System.out.println("这是创建的新线程执行的任务");
    10. }});
    11. Thread t1 = new Thread(() -> {while (true) {
    12. try {
    13. Thread.sleep(100);
    14. } catch (InterruptedException e) {
    15. throw new RuntimeException(e);
    16. }
    17. System.out.println("这是创建的新线程执行的任务");
    18. }});
    19. t.start();
    20. t1.start();
    21. System.out.println(t.getId());//获取并打印当前线程的ID
    22. System.out.println(t1.getId());//获取并打印当前线程的ID
    23. }

    b804bcb5dcb64387821b90b2bdf7d29e.png

    isDaemon()

    判断当前线程是否为后台线程。

    线程默认都是前台线程。

    • 前台线程:只要该进程中还有前台线程未执行完,那么该进程就不会结束(前台线程会影响进程的结束与否);
    • 后台线程:只要该进程中的前台线程都执行完毕,那么此时无论是否有未执行完的后台线程进程都会结束(后台线程不会影响进程的结束与否)。
    1. public static void main(String[] args) {
    2. Thread t = new Thread(() -> {while (true) {
    3. try {
    4. Thread.sleep(100);
    5. } catch (InterruptedException e) {
    6. //打印异常
    7. e.printStackTrace();
    8. }
    9. System.out.println("这是创建的新线程执行的任务");
    10. }});
    11. t.start();
    12. System.out.println(t.isDaemon());//获取并打印当前线程是否为后台线程
    13. }

    0529f0a3cabe4a7cb3c1601bc1ed7ca9.png

    此时将上述代码改成后台线程。

    1. public static void main(String[] args) {
    2. Thread t = new Thread(() -> {while (true) {
    3. try {
    4. Thread.sleep(100);
    5. } catch (InterruptedException e) {
    6. //打印异常
    7. e.printStackTrace();
    8. }
    9. System.out.println("这是创建的新线程执行的任务");
    10. }});
    11. //将当前线程设置为后台线程
    12. t.setDaemon(true);
    13. t.start();
    14. System.out.println(t.isDaemon());
    15. }

    8bd1e4d9722f48f899f64e0cb9d291f1.png

    此时因为主线程(main线程)飞快的执行完了所以没有任何打印。

    isAlive()

    分别在线程启动前后打印判断线程是否存活(注意:线程对象存活时线程并不一定会存活)

    1. public static void main(String[] args) throws InterruptedException {
    2. Thread t = new Thread(() -> {
    3. try {
    4. Thread.sleep(1000);
    5. } catch (InterruptedException e) {
    6. //打印异常
    7. e.printStackTrace();
    8. }
    9. System.out.println("新线程执行完毕");
    10. });
    11. System.out.println("线程启动前");
    12. System.out.println("线程是否存活"+t.isAlive());
    13. t.start();
    14. System.out.println("线程已启动");
    15. System.out.println("线程是否存活"+t.isAlive());
    16. Thread.sleep(2000);
    17. System.out.println("线程是否存活"+t.isAlive());
    18. }

    c5b74a4e3d544c9299f1eab678344a41.png

    根据结论可以得知当我们创建线程对象之后并不会调用系统API创建线程,只有当调用了start() 方法之后才会调用系统API在系统中创建出线程并启动。

    打断线程

    在JAVA中打断线程的方法是比较唯一的:本质上都是让run()方法尽快执行结束;而在C++中是有办法可以在线程执行过程中直接销毁该线程,但是这样做有个坏处比如这个线程在写文章时突然中断了那就会令这篇文章有头无尾,而在JAVA中就可以允许在此处进行一些收尾工作。

    而现实中令run()方法迟迟无法结束的原因一般都是应为循环,所以只要 结束循环就可以让线程尽快执行完run()方法,从而达到打断线程的效果。

    此处介绍两种方法:

    第一种方法:

    可以手动创建出一个标志位,用来控制 run() 方法中循环的终止条件。

    1. //创建一个成员变量用来控制循环的终止条件,默认值为 false;
    2. private static boolean isQuit;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t = new Thread(() -> {
    5. while(!isQuit) {
    6. System.out.println("新线程正在工作");
    7. try {
    8. Thread.sleep(100);
    9. } catch (InterruptedException e) {
    10. //打印异常
    11. e.printStackTrace(); }
    12. }
    13. System.out.println("新线程执行完毕");
    14. });
    15. t.start();
    16. Thread.sleep(500);
    17. isQuit = true;
    18. System.out.println("打断新线程");
    19. }

    cf6542b5d12c4b8abd3f50a8b87e8445.png

    但是有以下两个问题:

    1. 需要手动创建标志位;
    2. 如果循环正处在sleep状态程序将不能进行及时的响应。

    第二种方法:

    在JAVA中默认就有一个标志位,我们可以利用JAVA中默认的标志位来进行快速结束run()方法的操作。

    好处是这样我们就不用再单独创建一个变量,不用再思考变量捕获的问题了。

    • interrupt() :该方法可以将线程中默认的标志位设置为true
    • isInterrupt():判断对象关联的线程的标志位是否被设置,调用后不清除标志位。还可以使sleep()和wait()方法抛出InterruptedException异常,来强行中断sleep()和wait()方法。

    利用这两个方法就可以实现线程的打断

    1. public static void main(String[] args) throws InterruptedException {
    2. Thread t = new Thread(() -> {
    3. //Thread.currentThread()该方法是用来得到该线程的实例也就是t(哪个线程调用该方法就返回哪个线程的实例)
    4. //因为此时t还没有被创建所以不能写为t.isInterrupted()
    5. while(!Thread.currentThread().isInterrupted()) {
    6. System.out.println("新线程正在工作");
    7. try {
    8. Thread.sleep(1000);
    9. } catch (InterruptedException e) {
    10. //打印异常
    11. e.printStackTrace();
    12. }
    13. }
    14. System.out.println("新线程执行完毕");
    15. });
    16. t.start();
    17. Thread.sleep(5000);
    18. //设置标志位为true
    19. t.interrupt();
    20. System.out.println("打断新线程");
    21. }

    0a7d1cbb2cb043519d03f502c3c38b12.png

    结果可以看出来此时标志位确实被设置了,sleep()方法也抛出了异常,可是循环并没有被终止。

    原因是sleep()方法在抛出异常之后会自动将标志位清除,而此引起的结果就和没有设置标志位是相同的。

    而JAVA如此设计的原因其实就是扩大程序员的可操作空间,可以再sleep()方法抛出异常之后进行一些收尾工作。

    1. public static void main(String[] args) throws InterruptedException {
    2. Thread t = new Thread(() -> {
    3. //Thread.currentThread()该方法是用来得到该线程的实例也就是t(哪个线程调用该方法就返回哪个线程的实例)
    4. //因为此时t还没有被创建所以不能写为t.isInterrupted()
    5. while(!Thread.currentThread().isInterrupted()) {
    6. System.out.println("新线程正在工作");
    7. try {
    8. Thread.sleep(1000);
    9. } catch (InterruptedException e) {
    10. //此处可以写一些收尾工作的代码
    11. break;
    12. }
    13. }
    14. System.out.println("新线程执行完毕");
    15. });
    16. t.start();
    17. Thread.sleep(500);
    18. //设置标志位为true
    19. t.interrupt();
    20. System.out.println("打断新线程");
    21. }

    37fd158250634d449c15b256f84593f7.png

    线程等待

    再多线程的代码中由于线程的调度是随机的,所以也就会导致每个线程的结束时间也是无法预测的,而这种情况下就会使得在有些场景下代码出现BUG。

    而线程等待就是让一个线程来等待另一个线程执行结束,本质上就是来控制线程结束的顺序。

    join()

    实现线程等待的效果,让一个线程阻塞等待另一个线程执行结束之后再执行。

    • 等的线程:在哪个线程中调用 join 方法,哪个线程就阻塞等待;
    • 被等的线程:调用的哪个线程对象的 join 方法,哪个线程就是被等的线程,当这个线程执行完毕等的线程才会执行。

    我们创建一个线程 t 让这个线程每隔一秒打印一次数据,让主线程等待该线程。

    1. Thread t = new Thread(()->{
    2. for (int i = 0; i < 4; i++) {
    3. System.out.println("t线程执行中");
    4. try {
    5. Thread.sleep(1000);
    6. } catch (InterruptedException e) {
    7. throw new RuntimeException(e);
    8. }
    9. }
    10. });
    11. t.start();
    12. System.out.println("等待开始");
    13. t.join();
    14. System.out.println("等待结束");

    2c3946c9d755441484d21364a57922f3.png

    注意:如果 t 线程已经执行结束,此时再调用 join 就会直接返回执行,不会发生阻塞等待。

    join(等待时间)

    上面的 join() 方法是一种“死等”的方法,只要被等待的线程不结束,那么就会一直等待下去。

    但是一般情况下我们并不会死等而是等待超过了一定时间之后就不会再继续等待了,因为没有意义。

    • join(long millis)         最多等待 millis 毫秒
    • join(long millis, int nanos)       和上面的方法一样 就是时间精度更高,精确到了纳秒

    休眠线程

    • sleep(long millis)  让线程休眠 millis 毫秒;
    • sleep(long millis,int nanos)  和上面的功能一样,就是精度更高。

    但是下面的这一种其实意义不大,因为 sleep() 本身就存在一定的误差,并不是你写 sleep(1000) 就真的刚好等精确 1000 ms ,它还有一个调度的开销。系统会按照 1000 这个时间来休眠线程,当时间到了之后,系统会唤醒该线程(阻塞 -> 就绪),而且并不是线程进入就绪状态就能立即进入CPU执行。

    1. //获取系统当前时间戳
    2. long a = System.currentTimeMillis();
    3. Thread.sleep(1000);
    4. //获取系统当前时间戳
    5. long b = System.currentTimeMillis();
    6. System.out.println("时间:"+(b - a)+" ms");

    52b3dd8f39f849abb5d11fc648f39e8e.png

    而且每次运行之后结果都是不同的。

    54aa69af05034568b1a5dd85b80dc0af.png

    线程的状态

    JAVA中线程的所有状态都存储在一个枚举类型中,Thread.State :

    1. for (Thread.State str:Thread.State.values()) {
    2. System.out.println(str);
    3. }

    可以通过上述代码来打印所有的线程状态

    17e423c9afad444c9c7664b43406901b.png

    通过 getState() 方法可以获取线程的状态

    • NEW:Thread对象已经有了,但还没有调用 start() 方法;
    1. Thread t = new Thread(()->{
    2. });
    3. System.out.println(t.getState());

    84dd996330ca4313911fe67c34c5133a.png

    • RUNNABLE:就绪状态(线程已经在CPU上执行了或者排队准备执行)
    1. Thread t = new Thread(()->{
    2. while(true) {
    3. }
    4. });
    5. t.start();
    6. System.out.println(t.getState());

    c88cff52f82c4364a25871db4bc8de2f.png

    • BLOCKED:阻塞,由于锁竞争导致的阻塞
    1. Object lock1 = new Object();
    2. Object lock2 = new Object();
    3. Thread t1 = new Thread(()->{
    4. synchronized(lock1) {
    5. try {
    6. Thread.sleep(1000);
    7. } catch (InterruptedException e) {
    8. throw new RuntimeException(e);
    9. }
    10. synchronized(lock2) {
    11. }
    12. }
    13. });
    14. Thread t2 = new Thread(()->{
    15. synchronized(lock2) {
    16. try {
    17. Thread.sleep(1000);
    18. } catch (InterruptedException e) {
    19. throw new RuntimeException(e);
    20. }
    21. synchronized(lock1) {
    22. }
    23. }
    24. });
    25. t1.start();
    26. t2.start();
    27. //让主线程等待 2 秒
    28. Thread.sleep(2000);
    29. //此时t1和t2两个线程会因为互相争对方的锁,而导致死锁
    30. System.out.println(t1.getState());
    31. System.out.println(t2.getState());

    1383895c2cb345689981e925ddcdd38c.png

    • WAITING:阻塞由 wait 这种不固定时间的方式引起的阻塞
    1. Object lock1 = new Object();
    2. Thread t1 = new Thread(()->{
    3. synchronized(lock1) {
    4. try {
    5. //调用wait方法让线程阻塞
    6. lock1.wait();
    7. } catch (InterruptedException e) {
    8. throw new RuntimeException(e);
    9. }
    10. }
    11. });
    12. t1.start();
    13. Thread.sleep(1000);
    14. System.out.println(t1.getState());

    bccdeb32a01840498dfaa1a882f319f4.png

    • TIMED_WAITING:由 sleep 这种固定时间限制的方式引起的阻塞
    1. Thread t1 = new Thread(()->{
    2. try {
    3. Thread.sleep(3000);
    4. } catch (InterruptedException e) {
    5. throw new RuntimeException(e);
    6. }
    7. });
    8. t1.start();
    9. Thread.sleep(1000);
    10. System.out.println(t1.getState());

    84ebca74d7724f518e8ccf56ce4c2564.png

    • TERMINATED:Thread对象还在,可是线程已经没了
    1. Thread t1 = new Thread(()->{
    2. });
    3. t1.start();
    4. Thread.sleep(1000);
    5. System.out.println(t1.getState());

    a4d19a8accb640d3a5e29eabc93a7c6d.png

  • 相关阅读:
    C++基础教程(转载)
    ADAU1860调试心得(8)FASTDSP-0 通道输入
    从 WinDbg 角度理解 .NET7 的AOT玩法
    ICLR‘23论文得分排名! 多篇论文竟同时获1分和10分?
    element-plus 踩的坑
    期末前端web大作业:用DIV+CSS技术设计的动漫网站——火影忍者6页 带报告
    Java版本+企业电子招投标系统源代码+支持二开+招投标系统+中小型企业采购供应商招投标平台
    2041. 面试中被录取的候选人
    【JS学习】字符串的replace方法
    std::this_thread
  • 原文地址:https://blog.csdn.net/2302_76339343/article/details/133924126