JavaEE 初阶(4)—— 多线程2之实现多线程

  线程  本身是 操作系统 提供的概念,且 操作系统提供 API 供程序员调用。不同的系统,提供的 API 是不同的(Windows 创建线程的 API 和  Linux 差别非常大...)
  Java (JVM) 把这些系统 API 封装好了,不需要关注系统原生 API, 只需要了解好 Java 提供的这一套 API就行了 --> Thread 标准库

一. 线程的创建

1.通过继承Thread类
class MyThread extends Thread{
    @Override
    public void run(){
        //这里写的代码,就是即将创建出的线程,要执行的逻辑
        while(true){
            System.out.println("hello thread");
            //循环中,加上 休眠 操作,让循环每执行一次,都休息一会儿,避免 CPU 消耗过大
            try {
                Thread.sleep(1000);//sleep是Thread中的类方法,static修饰
            } catch (InterruptedException e) {
                //throw new RuntimeException(e);//抛出新的异常,且打印异常信息

                e.printStackTrace();//只是打印异常信息

                //实际开发中处理异常的方法有很多,不只上面两种

            }
        }

    }
}
public class Main1 {
    //调用main方法的线程称为主线程  一个进程至少一个线程-->主线程
    public static void main(String[] args) {
        //创建一个线程对象
        MyThread t = new MyThread();
        //创建并启动线程,执行run里面的代码
        t.start();

        //t.run()不会创建线程,依旧是在主线程中执行逻辑

        //主线程
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

结果解析: 多个线程之间,谁先去CPU上调度执行,这个过程是“不确定的”(不是数学意义的随机),这个调度顺序,取决于 操作系统 内核里的 “调度器”。调度器里有一套规则,但是作为应用程序的开发者 是感受不到的,也没法进行干预。决定调度执行哪一个线程的过程,是一个“抢占式执行” 的过程。


我们可以通过Java jdk 中的 bin包 下的 jconsole 查看线程情况。 

堆栈跟踪 是一个程序在运行时生成的诊断信息,它显示了程序执行到当前点时,调用栈上所有活动的帧(Frame)。每个帧代表一个方法调用,堆栈跟踪能够展示哪些方法被调用,以及调用它们的顺序。当异常发生时,堆栈跟踪尤其有用,因为它可以帮助开发者追踪异常的来源,即确定异常是在哪个方法中首次被抛出的。 

除了手动创建的线程和主线程, 剩下的线程,都是起到了一些辅助作用:a.  垃圾回收 (合适时机,释放不适用的对象,后面专门章节会介绍)  b. 统计信息/调试信息   比如 现在通过jconsole能够查看到一个Java进程的详情 

 

类似的信息也可以再IDEA的调试器看到:


注意:

  1. run方法中 处理的异常 不能 以throws的方式抛出。因为run是重写的方法,父类的run方法没有throws的声明,因此重写不可以增加throws,必须 try-catch 处理。
     
  2. 上述代码中,run方法并没有手动调用,但是最终也执行了。像这种用户手动定义,但是没有手动调用,最终这个方法被系统/库/框架进行调用了 --> 就称为“回调函数” (callback)

回调函数:

在之前的学习中已经出现过:

  1. C语言中 函数指针 主要有两个用途:
    a. 作为回调函数      b. 实现转移表--> 降低代码复杂度
  2. Java数据结构的优先级队列(堆)必须先定义好对象的“比较规则”
    Comparable中的compareTo方法      Comparator中的compare方法
    自己定义了,但是没有手动调用,此时都是由 标准库 本身内部逻辑负责调用的。

然而,过度使用回调函数也可能导致代码难以理解和维护,这就是所谓的回调地狱。js里面的回调太多了,为了解决这个问题,提供Promises、async/await等。


2.通过实现Runnable接口
//public interface Runnable{}
class MyRunnable implements Runnable{
    @Override
    public void run() {
        //描述了线程需要完成的逻辑
        System.out.println("hello thread");
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
           throw new RuntimeException(e);
        }
    }
}
public class Main2 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.start();

        while(true){
            System.out.println("hello main");
            try{
                sleep(1000);
            }catch (InterruptedException e){
                throw new RuntimeException(e);
            }
        }
    }
}

  通过 Thread创建线程,传入实现Runnable接口的对象,Runnable就是用来描述“要执行的任务是什么”,不是通过Thread自己来描述。

  有的人认为 Runnable 这种做法 更有利于“解耦合”。因为 Runnable 只是一个任务,并不是和“线程”这样的概念强相关的,后续执行这个任务的载体可以是线程,也可以是其他的东西:比如,后续会介绍 “线程池” (ThreadPool)来执行任务,再比如,可以通过“虚拟线程”(“协程”Coroutine )来执行

3.通过匿名内部类创建
public class Main3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            public void run(){
                while(true){
                    System.out.println("hello thread1");
                    try {
                        Thread.sleep(1000);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        };//匿名内部类,是Thread的子类,重写父类的run方法

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }//匿名内部类,是Runnable接口的子类,重写父类的run方法
        );

        t2.start();

    }
}

匿名内部类 一般是“一次性”使用的类,用完就丢了,内聚性 更好一些 。

4.Lambda表达式改写匿名内部类
public class Main4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello thread1");
                try {
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        t.start();
      
    }
}

lambda表达式是最常用的方法

二.线程调度常用方法

 构造方法

其中 通过起名的方法 可便于调试

ThreadGroup线程组 --> 把多个线程放到一个线程组里,方便统一设置线程的一些属性(现在会很少用到线程组,线程的相关属性用的不太多,用到更多的是“线程池”)

成员方法

getId() : Thread 对象的身份标识,JVM 自动分配的,不能手动设置
getName() :获得线程的名称
getState() :阻塞/就绪/等待....Java 中把线程的状态分的更详细(后面再详细介绍)
getPriority() :设置 不同的优先级 会影响到系统的调度。这里的影响 是基于“统计"规则的影响,直
接肉眼观察,很难观察到效果


 isDaemon():判断是否是后台线程 

后台线程:如果这个线程执行过程中,不能阻止进程结束(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就是"后台线程”,也叫“守护线程”(daemon)

前台线程:如果某个线程在执行过程中,能够阻止进程结束,此时这个线程就是"前台线程”。一般main线程和 创建的线程 默认为 前台线程,当新创建的线程 调用 setDaemon(true) 方法会 转为 后台线程

前台线程和后台线程,除了影响进程退出之外,其他的没啥别的区别了...

通俗来讲:

  • 后台线程
    1)前台线程宣布结束,此时进程就结束,后台线程也会随之结束
    2)前台线程不宣布结束,后台线程结束了也不受影响
  • 前台线程
    1)进程要结束(前台线程要结束) --> 无力阻止
    2)后台线程先结束了,不影响 进程的结束(其他前台线程的结束)

  一个进程中,前台线程可以有多个(创建的线程默认就是前台线程),必须所有的前台线程都结束,进程才结束。若后台线程没有执行完也会“强制结束”....

public class Main5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"自定义线程");

        //把t设置为守护线程,不能阻止主线程的结束
        t.setDaemon(true);
        t.start();

        //主线程不写循环,执行完就结束,t线程也会随之结束
    }
}

运行结果:

操作系统中 还有"前台进程”"后台进程"这个概念,Java 不做过多介绍,C++会介绍,和前台线程、后台线程没有关系。


 isAlive():为 true 表示内核的线程存在;为 false 表示内核的线程不存在了。
  代码中,创建的 Thread 对象的 生命周期 和 内核中实际的线程 是不一定一样的。可能会出现:Thread 对象仍然存在,但是内核中的线程不存在了这样的情况

  • 实例了Thread对象,没调用 start ,此时内核中 还没创建线程
  • 线程的 run 执行完毕了,内核的线程就没了,但是 Thread 对象 仍然存在

(注:不会出现 Thread 对象不存在,线程还存在这种情况) 

public class Main6 {
    public static void main(String[] args) throws InterruptedException {
        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();
                }
            }
        });
        //未调用start,线程未创建状态
        System.out.println(t.isAlive());//false

        t.start();
        System.out.println(t.isAlive());//true

        //等待4s,此时线程已经运行结束
        Thread.sleep(4000);
        System.out.println(t.isAlive());//false

    }
}

运行结果:

注:由于线程之间的调度顺序是不确定的,如果两个线程都是sleep(3000),当时间到,谁先执行 谁后执行 不一定(不是指双方概率均等,实际上这里的两种情况的概率,可能会随着系统的不同、代码运行环境的不同而存在差异)

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值