【多线程】多线程(3):线程等待,获取线程实例,线程状态,串行与并发执行

【线程等待】

操作系统针对多个线程的执行,是一个“随机调度,抢占式执行”,而「线程」等待,就是在确定两个线程的结束顺序,它无法控制两个线程调度执行的顺序,但是可以控制谁先结束,谁后结束

因此,让后结束的线程等待先结束的线程即可,此时后结束的线程会进入阻塞,直到先结束的线程真的结束了,阻塞才解除

【join()】

线程等待所需要用到的API,在一个线程中,谁去调用join,就是要这个线程去等待谁先结束

比如现在有两个线程A,B

在A线程中执行B.join(),意思就是让A线程等待B线程先结束,然后A线程再继续执行

在哪个线程中进行,就是让哪个线程去等待

Thread t = new Thread(() ->{
            for(int i = 0;i < 3;i++){
                System.out.println("这是线程 t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程t执行结束");
        });

        t.start();
        System.out.println("main线程开始等待");
        try {
            t.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("main线程结束等待");

主线程执行完start后,会立即执行打印“main线程开始等待”,随后执行到t.join(),这意味着主线程要等待到t线程结束

t线程中循环三次打印,可以看到在这个过程中主线程一直没有打印“main线程结束等待”

什么时候打印?持续到t线程打印“线程t结束”后(这意味着t线程结束了,join开始返回),此时主线程也就可以结束了

这说明在t线程结束之前,主线程一直在等待

//join的目的是确保被等待的线程可以先结束,如果在这之前已经先结束了,join就不必在等了

注意,任何线程之间都是可以相互等待的(不是说必须main线程才能等待别人),线程等待,也不一定是两个线程之间,一个线程可以同时等待多个别的线程,或者若干线程之间也能相互等待

Thread t1 = new Thread(() ->{
            for(int i = 0;i < 3;i++){
                System.out.println("hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1结束");
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 4;i++){
                System.out.println("hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t2结束");
        });

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

这段代码相当于:主线程既要等待t1,也要等待t2,只有t1,t2这两个线程都结束,主线程才能结束

 Thread t2 = new Thread(() ->{
            try {
                t1.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            for(int i = 0;i < 4;i++){
                System.out.println("hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t2结束");
        });

 如果在t2线程中加入了一个t1.join,这意味着t2线程要一直阻塞到t1线程执行完毕后才能执行

【无参数的等待】

join无参数,意味着“死等”,被等待的线程只要不执行完,这里的等待就会持续阻塞

这并不好,一旦被等待的线程代码出现bug,就可能令这个线程迟迟无法结束,从而令等待线程一直阻塞,无法执行其他操作

有参数的join,就不会死等了

参数的单位是“毫秒”,是给join设立的“超时时间”,如果达到了这个时间且线程还没有结束,那么也就不等了,直接返回

第三个方法是更精确的等待,第二个参数的单位是“纳秒”

【获取主线程实例的引用】

可以使用“Thread.currentThread()”,如果在线程中需要调用主线程.join,让主线程等待,就可以通过这个方法获取到主线程实例对象的引用

Thread mainThread = Thread.currentThread();

        Thread t = new Thread(() ->{
            System.out.println("t开始等待");
            try {
                mainThread.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t结束等待");
        });
        t.start();

        Thread.sleep(2000);
        System.out.println("main线程结束");

ps:任何线程中都可以通过这样的操作,拿到线程的引用

【再谈sleep】

线程执行sleep,就会让这个线程不参与cpu调度,从而把cpu资源让出来给别人使用

有的时候也把这种sleep操作称为“放权”,放弃使用cpu资源的权利

有的开发场景中,如果发现某个线程cpu占用率过高,就可以通过使用sleep来改善

【线程的状态】

Java中进程分为两种状态

就绪:正在cpu上执行,或者随时可以去cpu上执行

阻塞:暂时不能参与cpu执行

而线程的状态被细分为了很多种

通过t.getState()来获取t线程目前的状态

★NEW

当前Thread对象虽然有了,但内核的线程还没有(还未调用过start)

★TERMINATED

当前Thread对象虽然还在,但内核的线程已经被销毁(线程已经结束了)

★RUNNABLE

就绪状态:正在cpu上运行,或者随时可以去cpu上进行

★BLOCKED

阻塞状态,这是因为锁竞争引发的阻塞

★TIMED_WAITING:

阻塞状态,有时间限制的等待

★WAITING:

阻塞状态,没有时间限制的等待(永远也等不完)

【串行执行和并发执行】

多线程离不开线程安全问题,当多个线程同时执行某个代码时,可能引发较为奇怪的bug

Thread t1 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                count++;
            }
        });
        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                count++;
            }
        });

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

        System.out.println("count=" + count);

这个代码让两个线程分别令count循环自增50000次,理论上最终输出结果中count应该是100000次,但实际上输出的却是一个与预期相差甚远的数,而且每次执行得出来的输出结果各不相同

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

但如果调整t1,t2的start和join的位置,最终输出结果就符合预期了

为什么这个写法没问题呢?

这个写法,本质上相当于t1先执行,t1执行完了后t2再执行,t1和t2是串行执行的

但如果换刚开始的写法,本质上相当于t1和t2同时执行,t1和t2是并发执行的

因为多个线程并发执行引发的bug,称为“线程安全问题”或“线程不安全”

回过头来,此处代码中的count++操作,从cpu视角来看,是3个指令

1.把内存中的数据,读取到cpu寄存器中(load)

2.把cpu寄存器中的数据+1(add)

3.把cpu寄存器中的值写回内存中(save)

注意,这只是该笔记中的表述方式,不同架构的cpu有不同的指令集,不同的指令集中有不用的指令,针对这三个操作,不同cpu中对应指令的名称肯定是不同的

cpu在调度执行线程时,说不上什么时候,就会把线程切换走(随机调度,抢占式执行)

指令是cpu执行的最基本单位,要调度,至少把当前指令执行完,不会执行一半才调度走

但是由于count++是3个指令,可能会出现cpu才执行了其中1/2个指令就调度走的情况

基于以上情况,两个线程同时对count++,就容易出bug

以图为例:

正常情况下,t1执行完3个指令后,转到t2执行3个指令按照这个执行顺序的流程图

1.t1执行load,把内存中count的数值0提取到t1的寄存器中

2.t1执行add,把t1寄存器中的值增加,0变1

3.t1执行save,把t1寄存器中的1写回到内存中,此时count值由0变成了1

t2流程和t1亦相同

但发生bug时,可能会变成这样:

1.最开始count值为0,t2执行load,把内存中count的数值0提取到t2的寄存器中

2.t2执行add,把t1寄存器中的值增加,0变1

3.第三步并不是执行save,而是反而执行t1的load,把内存中count的数值0提取到t1的寄存器中

4.t1执行add,把t1寄存器中的值增加,0变1

5.t1执行save,把t1寄存器中的1写回到内存中,此时count值由0变成了1

6.t2执行save,我们可以看到t2寄存器中的值写回到内存,值还是1,这就出现问题了

上述过程中,明明是++了两次,结果却还是1,这两次++的结果出现了“覆盖”

以上列举的一些可能的的指令执行顺序中,只有打钩的这两种可以得到2,其他情况只会出现1

由于循环5w次的过程中不知道有多少次的执行顺序是打钩的情况,有多少次是其他情况,因此结果会是一个不确定值,且这个值一定小于10w

因此,“随机调度,抢占式执行”是对于多线程代码来说最大的困难

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值