【Java EE】-多线程编程(二) Thread类的几种方法+ wait和notify + 线程状态

作者:学Java的冬瓜
博客主页:☀冬瓜的主页🌙
专栏【JavaEE】
分享:愉快骑行的一天!
主要内容:Thread方法的使用,终止一个线程interrupt,等待一个线程join、休眠一个线程sleep、控制线程执行顺序wait和notify。
线程的六大状态:NEW RUNNABLE TERMINATED TIMEWAITING WAITING BLOCKED

在这里插入图片描述

一、Thread类的常用方法

1、Thread类的构造方法

注意:这样设置便于改线程名
在这里插入图片描述

2、Thread的几个常见属性

在这里插入图片描述

  • isDeamon()
    前台线程:会组织进程结束。只要前台线程还在运行,那么进程就不会结束,main以及我们手动创建的线程都是前台线程。可以用t.setDeamon()将前台线程改为后台线程。
    后台线程:也叫做守护线程,不会组织进程结束。即使守护线程还没运行完,进程也可以结束。
  • isAlive()
    isAlive 为true的条件是在PCB生命周期内
    在创建了Thread对象后,t.start()前,由于操作系统内核还没有创建Thread对象对应的PCB,所以此时isAlive是false。
    在t.start()后,在操作系统内核中,真正创建了线程Thread对应的PCB,因此isAlive为 true。
    当创建好的PCB在内存执行完后(run方法走完),PCB被销毁,此时isAlive 为false,而且此时PCB被销毁时,Thread对象可能还在。
    因为PCB创建前,和PCB销毁后,Thread对象都存在。因此,Thread对象比在内核中的PCB的生命周期长
  • isInterrupted()
    判断线程是否中断。

3、获取当前线程 类方法currentThread()

public static Thread currentThread();

4、终止一个线程

@ 法一:使用标志位来控制线程是否停止

  • 比如以下代码:标志位取false,意味着告诉t线程,你要结束了。
    缺点:无法及时响应,比如下面代码,加上sleep是为了控制执行次序。当刚到第三秒时,会执行run()方法,打印完后到第四秒开始时,main和t抢占式执行,才能继续main线程执行flag=false,而run()方法执行完。这样其实浪费了这1s的时间,对于计算机来说,这是一个很长的时间。
// 线程中断 法一:利用标志位来控制线程是否停止
public class ThreadTest {
    private static boolean flag = true;  // flag为标志位

    public static void main(String[] args) {

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag) {
                	System.out.println("Hello thread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"myThread");
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;   // 标志位取false,意味着告诉t线程要结束了
    }
}

@ 法二:使用当前线程自带的标志位控制线程中断(终止)

  • 使用Thread.currentThread().isInterrupted()代替自定义标志位;使用t.interrupt()终止线程
    特点:线程可以随时响应并中断执行,可以唤醒sleep操作。比如如果t线程在sleep中休眠,t.interrupt()会把t线程唤醒,告诉t线程,你要结束了,具体方式是抛出异常。
// 使用Thread自带的表示位和方法,控制线程终止
public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->
        {
            while (!Thread.currentThread().isInterrupted()){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("hello thread!");
            }
        });

        t.start();

        try {
            Thread.sleep(2300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 终止t线程
        t.interrupt();
    }
}
// 结果
hello thread!
hello thread!
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ThreadTest.lambda$main$0(ThreadTest.java:8)
	at java.lang.Thread.run(Thread.java:748)
hello thread!
hello thread!
hello thread!
...
  • 分析:可以在运行时发现t线程被提前唤醒抛出异常,还有在抛出异常后"hello thread"会一直打印下去,请看以下分析:

  • 当执行到第三秒时,打印了两个"hello world!",
    然后t线程休眠1s(其实只休眠了0.3),main线程再休眠0.3s,然后执行t.interrupt(),这时很明显的看到异常很快(0.3s)抛出,也就是说,t线程本来要休眠一秒,但是t.interrupt()让它提前抛出异常了。

  • interrupt()会做两件事:
    1> 把线程内部的标志位设置成true
    2> 如果线程在sleep,就会触发异常,把sleep唤醒。

  • sleep被唤醒时,会做一件事:
    sleep被唤醒时,sleep会把标志位再设置回false(清空标志位)(wait,join等也有相应操作)。因此,抛出异常后,还会循环打印下去,所以上面的代码其实是t线程忽略了终止请求。接下来是三种类型:

  • 程序员可以根据自己的需求
    忽略终止请求
    立刻执行终止线程
    先执行其它任务再终止线程

三种情况核心代码如下:
在这里插入图片描述

在这里插入图片描述

5、等待一个线程 — join()

  • 操作系统调度线程是抢占式执行,谁先谁后不知道。而join可以控制两个线程的结束顺序:让调用join()的线程先执行完,其它线程阻塞
  • 从以下代码和输出结果还有运行可以知道:
    在 t.join()前,t和main抢占式执行,所以"t——join之前"和第一个"hello thread"同时打印。
    在 t.join()后,先执行完 t,main才继续执行。t.join()其实就是让t线程调度,其它线程(比如main线程)进入阻塞,直到t线程执行完run方法,其它线程才继续往后执行。
    但是如果t.join()时,t线程已经结束,那t.join()就像是打游戏时空大了,其它线程并不会发生阻塞。
public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("t——join之前");
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t——join之后");
    }
}

结果:

t——join之前
hello thread
hello thread
hello thread
t——join之后
  • join()方法:
    在这里插入图片描述

6、休眠一个线程 类方法sleep()

public static void sleep(long millis);

理解:
在这里插入图片描述

二、wait和notify(Object类的方法)

1、wait和notify可以控制线程执行顺序,那是怎么控制的呢?

       来举个例子:比如 t1 t2两个线程要执行任务,要求
t1先执行的差不多了,再执行 t2。那此时,我们让 t2先wait(阻塞),等 t1执行的差不多了,再在 t1中通过 notify来通知
t2,把t2从阻塞中唤醒,继续执行 t2任务,这样就可以控制线程执行次序。
       当然,这个例子也可以用join完成,即在
t2线程run的第一行使用 t1.join(),就可以控制 t1在 t2前完成。但是当需求是
t1先执行50%,然后t2执行,那join就不能用了。而sleep不知道
t1执行50%具体消耗的时间,所以不能精确确定。然而,wait和notify就可以根据程序员自己决定。

2、wait和notify的使用方法

wait操作其实共要执行三个操作:
a> 释放锁
b> 进行阻塞等待
c> 收到notify的通知后,重新尝试获取锁,并且在获取锁后,继续往后执行。
在上面的操作中,还没获取锁,就想要阻塞等待,那就出问题了,所以报错:非法锁状态异常。

要求:t1先执行一半,再让 t2执行完,最后执行 t1的剩下的一半。

代码如下:

// wait 和 notify

public class Main {
    public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            synchronized (object){
                try {
                    System.out.println("t1 wait之前");
                    object.wait();
                    System.out.println("t1 wait之后");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (object){
                System.out.println("t2 notify之前");
                object.notify();
                System.out.println("t2 notify之后");

            }
        });
        t1.start();t2.start();
    }
}

结果:

t1 wait之前
t2 notify之前
t2 notify之后
t1 wait之后

需要注意的点:

  • a> wait和notify的对象必须是同一个。
    在这里插入图片描述

  • b> 在上面代码中,结果也可能为
    t2 notify之前
    t2 notify之后
    t1 wait之前
    然后就死等了
    因为在上面代码中t1和t2是抢占式执行的,我们无法确定谁先执行,如果是t2先执行,那t2先执行完前一半,然后notify就是空打了一炮,没有任何作用。然后就继续执行 t2剩下的一半,然后再调度到t1,执行t1的前一半,但是执行完t1的前一半后,t1就wait陷入死等了,没人将它唤醒。

  • c> 为了满足需求,让notify再wait之后执行,我们可以在t2最前面加上一个sleep,或者t1.start()和t2.start()中间加上sleep,那就可以大概率确保最开始是t1执行的。
    在这里插入图片描述
    在这里插入图片描述

  • d> 在上述代码中,wait是死等t2完成,t1才能完成剩下的一半,那我们可以给它传参,设置最大等待时间,如果超过这个等待时间,我们t1就会被唤醒。提前唤醒是正常操作,不会抛异常。但是interrupt唤醒sleep则是抛出异常。

  • e> 如果多个线程在阻塞等待object对象,此时有一个线程object.notify,那会唤醒任意一个在阻塞的线程(notifyAll则是唤醒全部正在阻塞的线程)。为了解决这样的问题,此时需要用到多个不同对象。

3、wait和notify小练习

要求:三个线程,使用notify,分别只能打印A,B,C,要保证是固定按照A、B、C的顺序打印的。

  • 使用两个对象加锁,每个对象供两个线程使用,连续的一个线程唤醒一个。具体代码如下:
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (o1){
                o1.notify();
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (o1){
                try {
                    o1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B");
            synchronized (o2){
                o2.notify();
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (o2){
                try {
                    o2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("C");
            }
        });
        t2.start();
        t3.start();
        Thread.sleep(100);  // 保证t2和t3的wait先执行,t1的notify后执行
        t1.start();
    }
}

三、线程状态

1、线程的六大状态

NEW:创建了Thread对象,但还没有start(操作系统内核中还没有对应的PCB)
TERMINATED:内核中PCB已经执行完,但是对应的Thread对象还在。
RUNNABLE:可运行的。正在CPU上执行的PCB或者在就绪队列中的PCB(线程)。

以下都是阻塞下的状态:,此时PCB在阻塞队列中。
TIMED_WAITING:runnable中,调用sleep方法对应的状态
WAITING:runnable中,调用wait方法时对应的状态
BLOCKED:runnable中,加锁后对应的状态。

线程的状态:
在这里插入图片描述
NEW RUNNABLE TERMINATED状态演示

// NEW RUNNABLE TERMINATED状态演示
public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            // 这个循环是确保start后,t获得RUNNABLE状态
            for (int i = 0; i < 1000000000; i++) {

            }
        });

        // start前,getState得到NEW状态
        System.out.println("start之前:"+t.getState());
        t.start();
        // 在start后,getState得到RUNNABLE状态
        System.out.println("start之后,t调度执行时:" + t.getState());

        // 加sleep让t线程执行完,再getState,就可得到TERMINATED状态
        // (不用sleep,使用join也行)
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t结束后:" + t.getState());
    }
}
start之前:NEW
start之后,t调度执行时:RUNNABLE
t结束后:TERMINATED

2、多线程的效率

  • 现在我们来看看,多线程和只有一个线程的执行效率的差别。在这之前我们先知道两个概念:
    CPU密集:包含了大量的 加减乘除 等运算。
    IO密集:包含读写文件,读写控制台,读写网络。比如启动eclipse时,需要加载数据,就涉及到大量的读写硬盘,阻塞了界面的响应,用多线程可以缓解。

业务:当前有两个变量,需要把两个变量各自自增100亿次(属于CPU密集)。代码如下:

// 演示单线程和多线程效率差别
public class ThreadTest {
    public static void main(String[] args) {
        serial();
        concurrency();
    }
	
	// 串行化
    public static void serial(){
        long begin = System.currentTimeMillis();

        long a = 0;
        for (long i = 0; i < 100_0000_0000L; i++) {  // int表示数据范围:-21亿~21亿,超过范围用long,且数值后加上L
            a++;
        }
        long b = 0;
        for (long i = 0; i < 100_0000_0000L; i++) {
            b++;
        }

        long end = System.currentTimeMillis();
        System.out.println("一个线程执行:"+(end-begin)+"ms");
    }

	// 并发执行
    public static void concurrency(){  //concurrency:并发的
        Thread t1 = new Thread(()->{
            long a = 0;
            for (long i = 0; i < 100_0000_0000L; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(()->{
            long b = 0;
            for (long i = 0; i < 100_0000_0000L; i++) {  // int表示数据范围:-21亿~21亿,超过范围用long,且数值后加上L
                b++;
            }
        });
        long begin = System.currentTimeMillis();
        t1.start();
        t2.start();

        // 确保t1和t2已经执行完
        try{
            t1.join();
            t2.join();
        }catch (Exception e){
            e.printStackTrace();
        }
		
		// 这时main从阻塞中恢复,继续执行下面代码:
        long end = System.currentTimeMillis();
        System.out.println("两个线程并发执行:"+(end-begin)+"ms");
    }
}

运行结果:从运行结果中可以发现,多线程效率确实比单线程快很多。

// 第一次
一个线程执行:8429ms
两个线程并发执行:3856ms
// 第二次
一个线程执行:8657ms
两个线程并发执行:4009ms
// 第三次
一个线程执行:9168ms
两个线程并发执行:3591ms
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学Java的冬瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值