【マルチスレッド】スレッドの状態

1.スレッドが参加するのを待ちます

ある日、張さんとシャオメイが家で夕食を食べていたとき、張さんは早く食事を終えて、シャオメイに「あなたが食べ終わったら、私は皿を洗いに行きます!」と言いました。

この時点で、スレッド A がスレッド B の実行が終了してからスレッド A が作業を続行できるように、Zhang San は Xiaomei が食事を終えるのを待ってから皿を洗う必要があります。

スレッドの待機とは、join メソッドを使用して、2 つのスレッドの終了順序を制御することです。

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 とメイン スレッドが同時に実行を開始します. メイン スレッドが t.join() まで実行する場合、メイン スレッドは t スレッドの実行が終了するのを待ってから t.join() に進む必要があることを意味しますコードを実行してください!t スレッドが実行されない限り、t スレッドが実行されるまで、つまり対応する run メソッドが実行されるまで、メイン スレッドはブロックされます。

上記のコードのように t スレッドが実行されるまで待つことができます. t スレッドが実行するタスクに無限ループがある場合はどうなりますか? このとき、メイン スレッドは延々と待たなければならないのでしょうか。これは確かに当てはまりますが、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("张三去洗碗了!");
}

この時、シャオメイは食事を終えていませんでしたが、張三はまだ皿を洗いに行っていました。なぜですか?

メイン スレッドは t スレッドを 5 秒間待機するだけですが、t スレッドによって実行される run メソッドは 5 秒以上続き、10 秒間スリープします。これは、Xiaomei が食事を終えるのに 10 秒に相当します!

今回は、上記の印刷された結果のように、Zhang San が皿洗いの前に Xiaomei が食事を終えるのを待たないことがわかります。

この時点で別の状況があります.XiaomeiはZhang Sanの前に食事を終えますか?

可能!たとえば、メイン スレッドが t.join() を実行し、t スレッドの run メソッドが実行された場合、この時点で結合はブロックされず、すぐに返されます。

同時に、join は 2 つのパラメーターを持つバージョンも提供します: 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