[Multihilo] El estado del hilo

1. Espera a que se una un hilo

Un día, Zhang San y Xiaomei estaban cenando en casa, Zhang San terminó la comida temprano y le dijo a Xiaomei: ¡Después de que termines de comer, iré a lavar los platos!

En este momento, Zhang San tiene que esperar a que Xiaomei termine de comer antes de poder lavar los platos, al igual que el hilo A tiene que esperar a que el hilo B termine de ejecutarse antes de que el hilo A pueda seguir funcionando.

Esperar hilos es controlar el orden final de dos hilos, utilizando el método de unión.

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("张三去洗碗了!");

Aquí, después de t.start(), t y el subproceso principal comienzan a ejecutarse simultáneamente. Cuando el subproceso principal se ejecuta en t.join(), significa que el subproceso principal tiene que esperar a que el subproceso t termine de ejecutarse antes de proceder a ejecutar el código! Mientras no se ejecute el subproceso t, el subproceso principal se bloqueará hasta que se ejecute el subproceso t, es decir, ¡se ejecute el método de ejecución correspondiente!

Al igual que el código anterior, puede esperar hasta que se ejecute el subproceso t. ¿Qué pasa si hay un bucle infinito en la tarea ejecutada por el subproceso t? En este momento, ¿el hilo principal tiene que esperar interminablemente? De hecho, este es el caso, pero join nos proporciona un método con parámetros, esperando el tiempo especificado, ¡y luego no esperaremos!

Por ejemplo, Xiaomei come muy lentamente. Zhang San dijo: Te esperaré durante 5 segundos. Después de 5 segundos, si no has terminado de comer, ¡lavaré los platos yo solo!

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("张三去洗碗了!");
}

En este momento, se puede encontrar que Xiaomei no terminó su comida, pero Zhang San todavía fue a lavar los platos, ¿por qué?

Porque el subproceso principal solo espera al subproceso t durante 5 segundos, pero el método de ejecución ejecutado por el subproceso t dura más de 5 segundos, y también duerme durante 10 segundos, ¡lo que equivale a 10 segundos para que Xiaomei termine su comida!

Se puede ver que esta vez Zhang San definitivamente no esperará a que Xiaomei termine de comer antes de lavar los platos, al igual que los resultados impresos arriba.

Hay otra situación en este momento, ¿Xiaomei terminará la comida antes que Zhang San?

¡posible! Por ejemplo, cuando el subproceso principal ejecuta t.join(), pero se ha ejecutado el método de ejecución del subproceso t, la unión no se bloqueará en este momento y volverá inmediatamente.

Al mismo tiempo, join también proporciona una versión con dos parámetros: public void join(long millis, int nanos) Esto puede esperar con mayor precisión, así que no diré más aquí.


2. Dormir el hilo actual

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

让线程休眠,本质上就是让这个线程不参与调度了(不去 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 核心都满载了,这个时候启动更多的线程也没啥用。

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


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

Supongo que te gusta

Origin blog.csdn.net/m0_61784621/article/details/129140048
Recomendado
Clasificación