线程的四种操作

所属专栏:Java学习****

1. 线程的开启

start和run的区别:

run:描述了线程要执行的任务,也可以称为线程的入口

start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api会在内核中创建线程)

start执行的速度是比较快的,一旦 start 执行完毕,新线程就会开始执行,调用start的线程,main线程也会继续执行

在Java中,一个Thread对象只能对应到一个系统中的线程,在start中就会根据线程的状态来判断,如果Thread对象没有start,此时的状态就是一个new状态,可以顺利调用start,如果已经调用过start,就会进入到其他状态,只要不是new状态,都会抛出异常

2. 线程的终止

当线程B正在运行时,如果发生了特殊情况需要终止掉线程,有两种实现方式:

  1. 通过共享的标记来进行沟通
  2. 调用interrupt()方法来通知

先来看使用自定义的isQuit作为标志位的例子:

java 复制代码
public class ThreadDemo4 {
    private static boolean isQuit = false;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run() {
                while (!isQuit){
                    System.out.println("thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("thread线程终止了");
            }
        };
        thread.start();
        Thread.sleep(1000);
        System.out.println("main线程尝试终止thread线程...");
        isQuit = true;
    }
}

这时就有一个问题需要注意:

如果isQuit不用static修饰,改为main方法里的局部变量可行不可行?ans:肯定是不可行的

这就涉及到了lambda表达式变量捕获的问题了

变量捕获是lambda表达式/匿名内部类中的一个语法规则:isQuit和lambda表达式定义在一个作用域中,此时lambda内部是可以访问到外部(和lambda同一个作用域)中的变量的,Java中的变量捕获是有一个特殊要求的,要求捕获的变量必须是final或事实final(虽然不是final修饰,但是后面没有更改)

就如上面的代码中,isQuit后面是被修改了的,所以就违反了语法规则

刚开始的形式为什么可以:写在外面就是外部类的成员变量,内部类本来就是可以访问外部类的

再来看interrupted的例子:

Thread类里面有一个boolean类型的成员变量interrupted,初始状态为false,一旦外面有线程调用interrupt方法,就会设置标志位为true

java 复制代码
public class ThreadDemo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            Thread currentThread = Thread.currentThread();
            while (!currentThread.isInterrupted()){
                System.out.println("thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
        Thread.sleep(1000);
        //主线程中控制thread被终止,设置标志位
        thread.interrupt();
    }
}

看起来是和自定义的标志位一样的,但是运行之后就会发现出现了异常:

由于在循环中判断和打印的操作太快了,整个循环的时间都是花在sleep方法里的,当main中调用interrupt时,大概率线程thread是在sleep中的,此时Interrupt就不仅能设置标志位,还可以唤醒thread线程,就会抛出InterruptedException异常,catch中捕获到异常也是做了抛出处理,就交给了JVM,程序就异常终止,那么把处理异常的方式更改一下试试:

这时就会发现while循环中的代码一直在执行

这里的原因是当sleep等阻塞函数被唤醒之后,就会清空刚刚设置的标志位,这时interrupted就一直是初始状态,也就导致了死循环,如果需要结束循环,可以把刚刚的异常处理直接改为break。

Java中终止线程的方式:A线程希望B线程能够终止,B线程收到这样的请求之后就可以自行决定是否是立即终止,稍后终止还是直接无视

  1. 如果B线程想要无视A,catch中就什么也不做,B线程就继续执行(sleep清除标志位)
  2. 如果B线程想要立即结束,就在catch中直接break或return
  3. 如果B线程想要稍后再终止,就可以在catch中添加其他的逻辑(释放资源,清理数据,提交结果...),这些完成之后再break/return.

3. 线程的等待

在之前提到过,操作系统针对多个线程的执行是一个"随机执行,抢占式调度"的过程,哪条线程先执行和先结束是不确定的,不过可以通过使用线程等待来决定哪条线程先结束,也就是让最后结束的线程等待先结束的线程,此时后结束的线程就进入了阻塞状态

例如在a线程中调用b.join(),就是让a线程等待b线程先结束,然后a再继续执行

java 复制代码
public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("thread1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread1结束了");
        });
        thread1.start();
        System.out.println("main线程开始等待");
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("main线程结束等待");
    }
}

上面的代码是thread1线程先执行,然后main线程开始等待,进入阻塞状态,如果说修改为先让thread1线程结束,main线程再开始等待会如何呢?

此时join并没有发生阻塞,join方法就是确保被等待的线程能够先结束,如果已经结束了,就没有等待的必要了

此外,任何线程都可以等待别的线程,而且可以等待多个线程,或者是多个线程之间互相等待

java 复制代码
public class ThreadDemo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 3;i++){
                System.out.println("线程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 < 3;i++){
                System.out.println("线程t2执行...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程t2结束");
        });
        t1.start();
        t2.start();
        System.out.println("main线程开始等待...");
        t1.join();
        t2.join();
        System.out.println("main线程结束等待");
    }
}

这就是main线程同时等待两个线程的例子,关于两个join的顺序其实没有区别,最终都是等待了4s

在main等待t1和t2同时,t2也可以等待t1:

这样就把原来t1和t2并发执行修改为了t1先执行

main线程也可以被其他线程等待,不过写法不同的是,需要先获取main线程的引用

java 复制代码
public class ThreadDemo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(()->{
            System.out.println("thread等待main线程");
            try {
                mainThread.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("结束等待");
        });
        thread.start();
        Thread.sleep(1000);
        System.out.println("main线程结束");
    }
}

在上面使用的join方法中,由于是没有传入参数的,就表示被等待的线程只要没有执行完,就会一直等待,这种方式肯定是不好的,如果被等待的线程出现问题了,就会使这个等待操作一直进行,所以就有了传参的版本,但上面列举的第三个高精度的一般也用不到

4. 线程的休眠

之前一直用的Thread.sleep()这个操作就是让调用的线程阻塞等待一定时间的,线程执行sleep之后,就会使这个线程不参与CPU的调度,把CPU的资源让出来,给其他线程使用,在开发时,如果发现某个线程的CPU占用率过高,就可以通过sleep来改善,虽然说线程的优先级也可以影响,但比较有限

相关推荐
渣哥1 天前
原来 Java 里线程安全集合有这么多种
java
间彧1 天前
Spring Boot集成Spring Security完整指南
java
间彧1 天前
Spring Secutiy基本原理及工作流程
java
Java水解1 天前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆1 天前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学1 天前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole1 天前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊1 天前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端
程序员鱼皮1 天前
刚刚 Java 25 炸裂发布!让 Java 再次伟大
java·javascript·计算机·程序员·编程·开发·代码