【多线程】线程的状态

1. 等待一个线程 join

有一天张三与小美在家里吃饭,张三早早的把饭吃完了,对小美说,等你把饭吃完了,我就去洗碗了!

此时张三就要等待小美吃完饭才能去洗碗,就好比线程 A 要等待线程 B 执行完,线程 A 才能接着往后干活!

等待线程,就是控制两个线程的结束顺序,使用 join 方法。

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        System.out.println("小美吃饭中!");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("小美吃完饭了!");
    });
    t.start();
    System.out.println("张三吃完饭了!");
    t.join(); // main 线程等待 t 线程执行完毕
    System.out.println("张三去洗碗了!");

这里当 t.start() 之后,t 和 main 线程就开始并发执行了,当 main 线程执行到 t.join() 表示 main 线程得等 t 线程执行完毕后,才能接着往后执行代码!只要 t 线程没有执行完毕,main 线程就会发生阻塞,一直阻塞到 t 线程执行完毕,也就是执行完对应的 run 方法!

像上述代码是可以等到 t 线程执行完毕了,如果 t 线程执行的任务里面出现了死循环怎么办呢?此时 main 线程是不是也得无休止的等待了?确实是这样的,但是 join 给我们提供了一个带参数的方法,等指定的时间,到点就不等了!

比如小美吃饭的速度特别慢,张三说,我就等你 5 秒钟,5 秒之后你还没吃完,我就自己洗碗了去了!

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        System.out.println("小美吃饭中!");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("小美吃完饭了!");
    });
    t.start();
    System.out.println("张三吃完饭了!");
    t.join(5000); // 只等待5秒
    System.out.println("张三去洗碗了!");
}

此时就能发现,小美没有吃完饭,但是张三还是去洗碗了,为什么呢?

因为 main 线程只等了 t 线程 5秒,但是 t 线程执行的 run 方法,可不止 5秒,里面还 sleep 了10秒钟,相当于小美还需要 10秒 才能吃完饭呢!

由此可见,这次张三肯定不会等小美吃完饭之后才去洗碗,就如同上面的打印结果一般。

此时还有一种情况,会不会小美比张三先吃完饭呢?

有可能!比如 main 线程执行 t.join() 的时候,但是 t 线程的 run 方法已经执行完了,此时 join 就不会阻塞了,就会立即返回!

同时 join 还提供了两个参数的版本:public void join(long millis, int nanos) 这个可以更高精度的等待,这里就不多说了。


2. 休眠当前线程

这个方法在前面也使用过,这里就来更细入的介绍一下。

让线程休眠,本质上就是让这个线程不参与调度了(不去 CPU 上执行了)。

通过调用 Thread 类中的 sleep 静态方法,传入指定时间,就能令线程休眠了。

注意:哪个线程里调用 Thread.sleep(1000),就让哪个线程休眠 1000 毫秒!

回顾一下上面讲过的知识,如果 main 线程中调用 t.join(),表示 main 线程要等待 t 线程结束,并且 main 线程进入阻塞状态。

而现在所讲的 sleep 方法,本质上是让线程休眠,也是进入了阻塞状态!

如何阻塞?这里需要了解两个玩意,就绪队列,阻塞队列。

当线程 A 调用 sleep() 方法,就会进入一个阻塞队列,当 sleep 结束,就会进入到就绪队列中。

阻塞队列里的线程,也就是暂时不参与 CPU 的调度了,就绪队列中的线程,随时可以被 CPU 调度!

所以一旦线程进入阻塞状态,对应的 PCB 就进入阻塞队列了, 因此暂时不能被 CPU 调度了,那如果线程阻塞结束,对应 PCB 进入到就绪队列,就一定会立即被 CPU 调度到吗?不一定!

这里一定要明确,CPU 是随机调度的!

这里来看一段代码:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        while (true) {
            System.out.println("hello");
        }
    });
    t.start();
    Thread.sleep(5000);
    System.out.println("main 继续执行");
}

这里 t 线程死循环打印 "hello",而 main 线程中休眠了 5000ms,那么一定刚好休眠 5000ms 后就打印 "main 线程继续执行" 吗?

不一定!此时虽然是 sleep(5000),但实际上考虑到调度的开销,对应线程是无法在唤醒之后就立即执行的,所以实际上的时间大概率要大于 5000ms。


3. 线程的状态

3.1 认识线程的六种状态

线程的状态是一个枚举类型 Thread.State,一个线程从创建到销毁都有着不同的状态。

这里就看下这个 State 枚举类型里面包含的所有状态:

public static void main(String[] args) throws InterruptedException {
    for (Thread.State s : Thread.State.values()) {
        System.out.println(s);
    }
}

NEW 状态:创建了 Thread 对象,但是还没有调用 start 方法(内核中还没创建 PCB)

TERMINATED:表示内核中的 PCB 已经执行完毕了(run执行完了),但是 Thread 对象还在

RUNNABLE:就绪状态,细分 1) 等待被 CPU 调度2) 正在 CPU 上执行,但在 Java 中,没有 运行时状态,都是 RUNNABLE(就绪) 状态

WAITING:调用 wait 或 join 时进入的状态,后面介绍

TIMED_WAITING:调用 sleep 进入的状态

BLOCKED:等待锁时产生的状态

最后这三个状态都是阻塞状态,都表示线程的 PCB 在阻塞队列中,只是为啥阻塞的原因不同而已,这些在后续内容中会详细展开介绍。

3.2 线程的状态转换

从这一节开始就提到过,一个线程从创建到销毁都有着不同的状态,下面就用一张简图和多段代码来理解线程工作中不同状态的转换。

下面就来看几段代码,通过代码来观察下线程的状态:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 10_0000; i++) {

        }
    });
    System.out.println("start 之前: " + t.getState());
    t.start();
    System.out.println("线程执行中的状态 : " + t.getState());
    Thread.sleep(10);
    System.out.println("线程结束后的状态 : " + t.getState());
}

当线程进入 TERMINATED 状态后,对应的 PCB 也就销毁了,也就无法再重新启动线程了,否则会抛异常,但是线程对应的对象 t 并没有被释放,我们仍然可以调用这个对象的一些方法属性,但是无法通过多线程来做一些事情了!

通过代码查看线程调用 sleep 进入 TIMED_WAITING 状态

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    for (int i = 0; i < 1000; i++) {
        System.out.println("t 的状态: " + t.getState());
    }
}

这里发现大量打印 TIMED_WAITING 状态是因为线程执行的 sleep(1) 休眠时间太长了(站在 CPU 的视角),CPU 1 毫秒可以做很多事,对于执行线程中的 for 循环来说,只会转瞬即逝,所以该线程大部分时间都在休眠,真正在 CPU 上执行的时间特别少,所以也就出现 TIMED_WAITING 状态比 RUNNABLE 多特别多!

目前只演示这四个状态,剩下的 WAITING,BLOCKED 等后续学习到了,会进行讲解。


4. 多线程的优势

前面讲了那么多多线程的理论知识,还很少写过代码,那么现在我们就来通过一小段代码来一下多线程的优势在哪。

● 现有两个变量 a,b 用两个循环对这两个变量分别自增 100 亿次:

单线程实现:

public static void main(String[] args) {
    long a = 0;
    long b = 0;
    long begin = System.currentTimeMillis(); //开始时间
    for (long i = 0; i < 100_00000000L; i++) {
        a++;
    }
    for (long i = 0; i < 100_00000000L; i++) {
        b++;
    }
    long end = System.currentTimeMillis(); //结束时间
    System.out.println(end - begin + " ms");
}
// 打印结果:5097 ms

多线程实现:

public static void main(String[] args) throws InterruptedException {
    long begin = System.currentTimeMillis(); //开始时间
    Thread t1 = new Thread(() -> {
        int a = 0;
        for (long i = 0; i < 100_00000000L; i++) {
            a++;
        }
    });
    Thread t2 = new Thread(() -> {
        int b = 0;
        for (long i = 0; i < 100_00000000L; i++) {
            b++;
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis(); //结束时间
    System.out.println(end - begin + " ms");
}
// 打印结果:2817 ms

上述两种实现方式,在不同配置的电脑上,执行的时间都不一样,但多线程肯定是比单线程要快的,但这里有个疑问,为啥多线程消耗的时间不是单线程消耗时间的一半呢?

按道理来说,如果单线程跑两个循环需要 5000ms,那用两个线程一个线程跑一个循环,不应该是 2500ms 吗?

实际上,t1 和 t2 在执行过程中,会经历很多次调度,有时候是并发执行的(两个线程在一个核心上快速切换),有时候是并行执行的(两个线程在不同的核心上执行),至于什么时候是并发,什么时候是并行?这些都是不确定的,取决于系统的配置,也取决于当前程序运行的环境,如果同一时刻你电脑上跑的程序很多,此时并行的概率就更小了。另外线程的调度也是有开销了,所以上述多线程版本执行时间肯定不会比单线程版本缩短 50%。

通过上述案例,不难发现在这种 CPU 密集型的任务中,多线程有着非常大的作用,可以充分利用 CPU 多核资源,从而加快程序的执行效率。

多线程一定能提高效率吗?

其实也不一定,如果电脑配置特别特别低,CPU 只有双核,其实也提高不了多少。

如果当前电脑跑了特别多程序,CPU 核心都满载了,这个时候启动更多的线程也没啥用。

学习到现在,看似多线程很美好,但殊不知有一个很大的危险正在向我们靠近,这也是多线程编程中让程序猿苦恼的事情,同时也是面试官经常问的问题 —— 线程安全!


下期预告:【多线程】线程安全问题

猜你喜欢

转载自blog.csdn.net/m0_61784621/article/details/129140048