【Java多线程编程全解析:从基础到AI应用】

](https://img-home.csdnimg.cn/images/20220524100510.png#pic_center)
🌈个人主页: Aileen_0v0
🔥热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法
💫个人格言:“没有罗马,那就自己创造罗马~”

多线程代码

  • 线程:线程本身是操作系统提供的,操作系统提供了API让我们操作线程。

  • JVM对操作系统的api进行了封装。

  • 线程在Java中提供了Thread类,表示线程。
    在这里插入图片描述
    在这里插入图片描述

  • 这重写的run方法是由Thread这个类提供的,让我们简单重温一下,重写和重载这两者的区别:

    • 重载:同一个作用域当中,多个方法之间,名字相同,参数列表不同。
    • 重写:父类和子类之间,父类有一个方法,子类也搞了一个一样的方法,子类的方法有时可能会替换掉父类方法(动态绑定) 。
      -run方法的作用:描述线程,具体要干什么。
package thread;

class MyThread extends Thread{
    @Override
    public void run() {
        //这里写的代码就是该线程要完成的任务
        System.out.println("Hello thread");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

在这里插入图片描述

在这里插入图片描述

  • 上面的代码中,有两个线程:

    • 1.t线程
    • 2.main方法所在的线程(主线程)
      • main进程是jvm进程启动的时候,自己创建的进程。
  • 一个进程中至少要有一个线程,以前写的代码都属于是一个进程只有一个线程的代码。上面这个程序就是一个进程中有两个线程。

  • 为了更加明显的展示出多线程的效果,我们在 main方法和run方法里面加上死循环来打印这两个线程的执行效果。

package thread;

class MyThread extends Thread{
    @Override
    public void run() {
        //这里写的代码就是该线程要完成的任务

        while (true){
            System.out.println("Hello thread");
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        while(true){
            System.out.println("Hello main");
        }
    }
}

在这里插入图片描述

  • 通过运行效果,我们可以看到这两个线程在并发执行。

Java中查看代码中线程工具jconsole

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 我们可以看到刚刚运行的程序消耗了大量的CPU资源,主要是因为while - true循环太快。
    在这里插入图片描述

sleep方法:就是让线程主动进入“阻塞状态”,主动放弃去cpu上执行(暂停了,先不往下走)。时间到了之后,线程才会解除阻塞状态(就是pcb上的状态属性),重新调度到cpu上执行。
在这里插入图片描述

受查异常

在这里插入图片描述

  • 通过上面运行结果可知,如果我们只用sleep方法,会抛出受查异常,所以我们需要给它加上异常捕获的try-catch代码块对异常进行捕获。

  • 修改后的代码块

package thread;

class MyThread extends Thread{
    @Override
    public void run() {
        //这里写的代码就是该线程要完成的任务
        while (true){
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        while(true){
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

在这里插入图片描述

在这里插入图片描述

  • 根据上面的运行结果我们可以看到,我们加上sleep以后,CPU的资源大幅降低了。

在这里插入图片描述

  • 根据打印结果我们可以看到,每一秒打印的时候,可能是main在前头,也可能是thread在前头。
  • 多线程的调度是“无序”的,在操作系统内部称为“抢占式执行”,任何一个线程,在执行到任何一个代码的过程中,都可能被其他线程抢占掉它的CPU资源,于是CPU就给别的线程执行了(就像是小朋友抢占摇摇车那样)。这样的抢占式执行充满了随机性,这使得多线程的执行效果,难以预测,可能会引起Bug。
    • 像主流的(Windows、Linux操作系统)都是这种抢占式执行的。
    • 也有小众的系统(发射卫星,火箭通过实时操作系统进行),通过“协商式”进行调度。
      • 实时操作系统:像这种实时操作系统是牺牲了很多功能换来的调度的实时性。
      • 抢占式调度:调度时间在极端情况下下是不可控的,(当线程特别多的时候,调度时间花的就多,容易产生误差)。

为什么调用的不是run方法

在这里插入图片描述

  • 记得小学的时候,我们每天学校都有作业,像我这种记性不是很好的(记性不好是理由,主要是不想写作业🐶)需要拿个本子记作业(也就是通过使用run方法记录下我每天要做的作业),然后一回到家就赶紧做作业(调用start方法去触发写作业这个动作)【start这个方法是Thread类自带的】

  • 此处的run只是定义出来的,并未真正去调用,run方法不是被start去调用的,run是被start创建出来的线程,它是在线程里面被调用的(要start写作业前提要先通过run去记录作业)
    在这里插入图片描述
    在这里插入图片描述

  • 上面两幅图展示了main线程和run线程是两个不同独立的调用栈,两者之间互不影响。

在这里插入图片描述

在这里插入图片描述

  • 如果我们直接通过主线程中创建的对象t去调用run线程的话,我们其实没有创建出新的线程,单纯就是在主线程中执行run方法中的循环打印
  • 通过上面的运行结果我们可以知道:调用 run() 方法不会创建新线程,而是在当前线程中执行 run() 方法(此时run和主线程的循环是串行执行,而不是并发执行,必须要等run中的循环结束,才能执行到下一个循环。)而调用 start() 方法会创建一个新线程,并在这个新线程中执行 run() 方法。这就是为什么在多线程编程中推荐使用 start() 方法来启动线程,因为它允许线程并行执行,这是多线程编程的核心目的之一。

回调函数

  • 回调函数:之前我们通过start去创建新线程,然后通过系统去调用run这个方法,而不是像上面那样直接在main方法中手动调用run方法。【像这样的,把这个方法的调用交给系统/其它的库/其它框架 来调用这样的方法(函数) 称作“回调函数(callback function)”】

⭐️⭐️⭐️⭐️⭐️创建线程的方法(5种)

①创建Thread子类,重写run方法。

package thread;

class MyThread extends Thread{
    @Override
    public void run() {
        //这里写的代码就是该线程要完成的任务
        while (true){
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        
        while(true){
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

②通过实现Runnable接口创建线程

package thread;

class MyRunnable implements Runnable{

}

public class Demo2 {
}

在这里插入图片描述

  • 通过Runnable的源代码我们可以看到,Runnable的作用是描述一个任务。这个任务 和 具体的执行机制(通过线程方式执行,还是通过其它方式执行)无关。
    • 其中 run 也就是要执行的内容本身。
package thread;

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();

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

上面两种方式创建线程的区别

在这里插入图片描述

  • 方式①:Thread自己记录并完成自己的作业。
  • 方式②:通过Runnable记录作业是什么,Thread负责执行。(别人替你记下了作业,然后Thread只需要完成作业即可)。
    • 引入方式②的原因:
      • 为了解耦合:将内容和线程这个概念拆分开,这样的任务,就可以给其他地方来进行执行。
      • eg:当前是通过多线程方式来执行的,未来也可以很方便改成基于线程池方式的执行,也可以基于虚拟线程的方式执行。

③通过匿名内部类来实现【针对①的变形,继承Thread重写run方法】(本质上就是①和②,但是换一个写法)

  • “匿名内部类”:这个类没有名字并且定义在别的类里面

在这里插入图片描述

在这里插入图片描述

  • 上面的操作:
    • 1.创建一个Thread的子类 (不知道啥名字,匿名)。
    • 2.同时创建了一个该子类的实例 t,对于匿名内部类来说,只能创建这一个实例。这个实例创建完以后,再也拿不到这个匿名内部类了。
    • 3.此处的子类内部重写了父类的run方法。
package thread;
public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            public void run(){
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

  • 上面代码也可以不创建实例去直接调用start方法来创建新线程,如下所示:
package thread;
public class Demo3 {
    public static void main(String[] args) {
        new Thread(){
            public void run(){
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();

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

在这里插入图片描述

④匿名内部类(针对Runnable)

package thread;

public class Demo4 {
    public static void main(String[] args) {
		//匿名内部类写法
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        t.start();

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

    }
}

在这里插入图片描述

在这里插入图片描述

  • 上面的操作:
    • 1.创建新的类,实现Runnable,但是类的名字是匿名的。
    • 2.创建了这个新类的实例 (一次性)。
    • 3.重写run方法。

⑤使用Lambda表达式

  • Lambda表达式 本质上是匿名内部类的平替。
  • Lambda本质上是一个一次性函数,用完就丢。
package thread;
public class Demo5 {
    public static void main(String[] args) {
//        Thread t = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                while (true) {
//                    System.out.println("Hello thread");
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        throw new RuntimeException(e);
//                    }
//                }
//            }
//        });
		//Lambda表达式的写法
        Thread t = new Thread(()-> {
                while (true) {
                    System.out.println("Hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
        });
        
        t.start();

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

在这里插入图片描述

小结:

  • 上述5种线程的实现方式,本质都是:要把线程执行的任务内容表示出来。
  • 通过Thread的start来创建/启动系统中的线程。【Thread对象和操作系统内核中的线程是一一对应关系】

异常抛出的细节

catch后面的内容

在这里插入图片描述

  • 自定义的catch后面的内容可以是:
    • (1)打印一些日志,把出现异常的详情都记录到日志文件里面。
    • (2)触发重试类的操作
    • (3)触发一些“回滚”类的操作
    • (4)触发一些报警机制(给程序员发短信/打电话,告诉程序员程序出问题。)

抛出异常的方式

在这里插入图片描述

  • 上面是main方法处理sleep异常的两种选择:
    • ①throws
    • ②try - catch

在这里插入图片描述

  • 上面是在线程run方法中的抛出异常方式只有:try - catch。

  • 为什么会出现上面的两种不同异常抛出方式的不同呢?

    • 根据上面的提示,我们可以看到throws是方法签名(method signature)的一部分,它包含:
      • 1.方法名字
      • 2.方法的参数列表(类型和个数)
      • 3.声明抛出的异常
    • 不包含:
      • 1.返回值
      • 2.public / private
    • 由于我们的run方法是重写自父类的run方法,我们去看看源码是如何编写的。
      在这里插入图片描述
  • 在Java中约定:方法在重写的时候,要求方法的签名是一样的,根据源码我们可以看到:父类的run方法中没有抛出异常,所以在子类重写的run方法中,也就无法抛出异常了。此外Thread是标准库的类,我们无法对它进行修改。

在这里插入图片描述

上面讨论的异常都是在javac编译器上的规则,和jvm没有任何关系。

小结:

  • 异常抛出的规则:父类要和子类一致,父类有异常声明,子类才能有,父类没有,子类也没有。Thread是标准库的类,没法对其进行修改,所以当子类重写它的run方法的时候我们不能通过throws来抛出异常。

未来实际开发中,服务器程序消耗的cpu资源超出预期,应该如何排查?

  • ①需要先确认,是哪个线程消耗的CPU比较高,未来会涉及到第三方工具,可查看每个线程CPU的消耗情况。
  • ②确定之后,进一步排查,线程中是否有类似的,“非常快的”循环。
  • ③确认清楚,这里的循环是否应该这么快,
    • 如果应该,说明我们需要升级更好的CPU
    • 如果不应该,说明需要在循环中引入一些“等待”操作(不一定是sleep

](https://img-home.csdnimg.cn/images/20220524100510.png#pic_center)
](https://img-home.csdnimg.cn/images/20220524100510.png#pic_center)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

I'mAileen

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

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

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

打赏作者

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

抵扣说明:

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

余额充值