zl程序教程

您现在的位置是:首页 >  其他

当前栏目

多线程的Thread 类及方法

2023-04-18 15:37:51 时间

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:海压竹枝低复举,风吹山角晦还明。

🌲一. 线程的复杂性

多线程是非常复杂的,以至于程序猿为了规避多线程代码而发明了很多其他的方法,来实现并发编程。

最复杂的地方实际上在于操作系统的调度执行

例如:主线程运行创建新线程,主线程中要完成 abcdef 个任务,而新线程要完成 123456 个任务

情况一:(时间线从上到下,按照时间先后顺序执行)

在这里插入图片描述

情况二:(时间线从上到下,按照时间先后顺序执行)

在这里插入图片描述

情况三:(时间线从上到下,按照时间先后顺序执行)

在这里插入图片描述
还有许许多多的情况,远远不止上面三种多线程最复杂的地方就在于操作系统 的 "随机" 调度,也可以理解为线程的创建是有先后顺序的,但是执行线程先后顺序是随机的!!!具体这个线程里的任务啥时候执行要看调度器!!!

 

🌴二. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

在这里插入图片描述

📕2.1 Thread 的常见构造方法

方法说明
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(), “这是我的名字”);

Thread(String name) --> 给线程起名字!线程在操作系统内核里,是没有名字的,只有一个身份标识,但是在 Java 中,为了能让程序猿调试的时候方便理解这个线程是谁,就在 JVM 中对应的 Thread 对象加了个名字(多个线程名字可以相同,不取名字也会有默认的名字)

public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }," 我的线程 ");
        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

例如上述代码中我们把线程命名为 " 我的线程 " 之后,我们可以根据上篇博客查看进程信息,在这个地方省略步骤。

在这里插入图片描述

如上我们就是进程命名成功了。

📗2.2 Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题后续详解

getId() --> 线程在 JVM 中的身份标识
 
线程的身份标识有好几个

  • 内核的 PCB 上有标识
  • 到了用户态线程库里也有标识。(pthread,操作系统提供的线程库)
  • 到了 JVM 中又有一个标识(JVM Thread 类底层也是调用操作系统的 pthread 库)
     
    标识各不相同,但是目的都是作为身份的区别

getName() --> 在 Thread 构造方法里传入的名字

getState() --> PCB 里有个状态,此处得到的状态是 JVM 里面设立的状态体系,比操作系统里的状态体系要丰富一些

getPriority() --> 获取到优先级

isDaemon() --> 守护线程(后台线程)。类似于手机 APP 前后台,线程分为前台线程和后台线程。一个线程创建出来默认是前台线程,前台线程会阻止进程结束,进程会保证所有的前台线程都执行完了才会退出;后台线程不会阻止进程结束,进程退出的时候不管后台线程是否执行完。 main线程就是一个前台进程,通过 t.setDaemon(true); 把线程设置为后台线程。

public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
           while(true) {
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        },"我的线程");

        t.start();
        System.out.println(t.getId());
        System.out.println(t.getName());
        System.out.println(t.getState());
        System.out.println(t.getPriority());
        System.out.println(t.isDaemon());
        System.out.println(t.isAlive());

    }
}

在这里插入图片描述
上述操作都是获取到一瞬间的状态,不是持续状态

📘2.3 启动一个线程-start()

创建 Thread 实例,并没有真的在操作系统内核里创建出线程!调用 start 才是真正在系统里创建出新的线程,才真正开始执行任务!调用 start 方法, 才真的在操作系统的底层创建出一个线程。

创建 Thread 实例 --> 安排任务,各就各位。任务内容的体现在于 Thread 的 run 或者 Runnable 或者 lambda 。

线程的执行结束:只要让线程的入口方法(run,Runnable,lambda)执行完了,线程就随之结束了。(主线程的入口方法,就可以视为是 main 方法)

📙2.4 中断一个线程

  1. 手动创建标志位,来区分线程是否要结束

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.

public class Demo9 {
    //用一个布尔变量表示线程是否要结束
    //这个变量是一个成员变量,而不是局部变量
    private static boolean isQuit = false;

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(!isQuit){
                System.out.println("线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("新线程执行结束!");
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("控制新线程退出!");
        isQuit = true;
    }
}

在这里插入图片描述

如上代码中控制线程结束,主要是这个线程有个循环,这个循环执行完毕就结束了。很多时候创建线程都是让线程完成一些比较复杂的任务,往往都是有一些循环(正是有这些循环,执行的时间才可能比较长一点),如果线程本身执行的很快,刷一下就完了,也就没有必要提前控制他结束的必要了。

Thread 中有内置的标志位,不需要咱们手动创建

  1. 使用 Thread 自带的标志位

在循环条件中改一下即可

Thread.currentThread().isInterrupted()

currentThread() --> 静态方法,获取到当前线程的实例(Thread 对象),这个方法总是会有一个线程调用他。线程 1 调用这个方法,就能返回线程 1 的 Thread 对象;线程 2 调用这个方法,就能返回线程 2 的 Thread 对象。
 
isInterrupted() --> 判定内置的标志位,为 true 表示线程要被中断,要结束。

public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        Thread.sleep(5000);
        System.out.println("控制线程退出!");
        t.interrupt();
    }
}

在这里插入图片描述

调用 interrupt,产生了一个异常!异常出现了,线程还在运行!

注意:interrupt 方法的行为

  • 如果 t 线程没有处在阻塞状态,此时 interrupt 就会修改内置的标志位
  • 如果 t 线程正在处于阻塞状态,此时 interrupt 就让线程内部产生阻塞的方法,例如 sleep 抛出异常,interruptedException。

此处异常被 catch 捕获了,捕获之后,什么都没做,就只是打印个调用栈就完了,因此把异常的打印忽略即可。

正是因为这样的操作,程序猿就可以自行控制线程的退出行为了

  • 可以立即退出
break;
  • 可以等一会儿退出
try {
    Thread.sleep(1000);
} catch (InterruptedException ex) {
    ex.printStackTrace();
}
break;
  • 可以不退出
啥也不做,相当于忽略了异常

主线程发出 “退出” 命令时,新线程自己来决定如何处理这个退出行为

综上:Java 中终止新线程
线程阻塞:1. 立即停止 2. 待会停止 3.啥也不做
线程没有阻塞:通过标志位直接停止

📓2.5 等待一个线程-join()

线程之间的执行顺序是完全随机的,看系统的调度!我们不能确定两个线程的开始执行顺序,但是可以控制两个线程结束的顺序!

join 的顺序谁先调用谁后调用是无所谓的

t1.join();
t2.join();

如上先调用 t1.join 后调用 t2.join ,此时 t1 t2 开始运行了,main 先阻塞在 t1 这里

  1. 如果是 t1 先结束,t2 后结束。当 t1 结束的时候,main这里的 t1.join 就执行完毕了,继续执行 t2.join,就阻塞了,然后 t2 结束的时候,main 的 t2.join 也返回(结束阻塞),main 继续执行,完成计时操作。
  2. 如果是 t2 先结束,t1 后结束。当 t2 结束的时候,由于 t1 没结束,main 仍然在 t1.join 这里阻塞,当 t1 结束之后,t1.join 解除阻塞,main 继续执行到 t2.join,由于 t2 已经结束了,t2.join 就不再阻塞了,直接返回,main继续执行
  • 实现让 t2 等待 t1执行完,main 等待 t2 执行完
public class Demo11 {
    private static Thread t1 = null;
    private static Thread t2 = null;

    public static void main(String[] args) {
        t1 = new Thread(()->{
            System.out.println("t1 begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
        });
        t1.start();

        t2 = new Thread(()->{
            System.out.println("t2 begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 end");
        });
        t2.start();

        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end");
    }
}

  • 主线程先执行 t1 然后 join 等待 t1 执行完毕,t1 执行完之后,主线程再启动 t2 等待 t2 执行完毕
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main begin");
        Thread t1 = new Thread(()->{
            System.out.println("t1 begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
        });
        t1.start();
        t1.join();

        Thread t2 = new Thread(()->{
            System.out.println("t2 begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 end");
        });
        t2.start();

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

join 的行为:

  • 如果被等待的线程还没执行完,就阻塞等待
  • 如果被等待的线程已经执行完了,直接就返回
方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度
  • 第一个是死等
  • 第二个和第三个设定了最大等待时间
  • 第三个的俩参数第一个是毫秒,第二个是纳秒,然后就是总的时间

未来实际开发程序的时候一般不会使用死等,会因为小的代码 bug 会让服务器卡死导致无法继续工作

📔2.6 获取当前线程引用

为了对线程进行操作(线程等待,线程中断,获取各种线程的属性),就需要获取到线程的引用

  • 如果是继承 Thread,然后重写 run 方法,可以直接在 run 方法中使用 this 即可获取到线程的实例,但是如果是 Runnable 或者 lambda,this 就不行了(this 就不是指向 Thread 实例)
  • 更通用的办法,Thread.currentThread()。哪个线程来调用这个方法,得到的结果就是哪个线程的实例。

📒2.7 休眠当前线程

  • 使用 sleep

在操作系统内核中有就绪队列(这里的PCB随时可以去CPU上执行),还有阻塞队列(这里的PCB暂时不参与调度,不去CPU上执行)。当某个代码中的线程调用 sleep 这个线程就从就绪队列跑到阻塞队列中,暂时不参与调度,等待 sleep 时间执行结束,然后就会调回就绪队列中(不是立马上 CPU 执行,还得看调度器的情况)。

 

🌳三. 线程的状态

🌻3.1 线程的所有状态

  • NEW: 安排了工作, 还未开始行动(创建了 Thread 对象,但是还没有调用 start 方法,系统内核里还没有线程)
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作(就绪状态:1. 正在 CPU 上运行;2. 还没在 CPU 上运行,但是一切准备就绪)
  • BLOCKED: 这几个都表示排队等着其他事情(等待锁)
  • WAITING: 这几个都表示排队等着其他事情(线程中调用了 wait)
  • TIMED_WAITING: 这几个都表示排队等着其他事情(线程中通过 sleep 进入的阻塞)
  • TERMINATED: 工作完成了(系统里面的线程已经执行完毕,销毁了,相当于线程的 run 执行完了,但是 Thread 对象还在)
public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{

                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

        });
        //start 之前获取,获取到的是线程还未创建的状态
        System.out.println(t.getState());

        t.start();
        Thread.sleep(500);//加上之后就是由 RUNNABLE 改成了 TIMED_WAITING
        System.out.println(t.getState());
        t.join();

        //join 之后获取,线程结束之后的状态
        System.out.println(t.getState());
    }
}

Thread.sleep(500);之前:(正在工作中)

在这里插入图片描述

Thread.sleep(500);之后:(正在 sleep 中)

在这里插入图片描述

🌹3.2 线程状态和状态转换

在这里插入图片描述
看起来复杂,简化起来就很简单:
在这里插入图片描述

主干道是 NEW => RUNNABLE => TERMINATED

在 RUNNABLE 会根据特定的代码进入支线任务,这些支线任务都是 "阻塞状态",这三种阻塞状态,进入的方式不一样,同时阻塞的时间也不同,被唤醒的方式也不同。