🌈个人主页: 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
方法。
- 1.创建一个
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
)