网站建设理由盐酸达泊西汀片是治疗什么的药物
大家好呀
我是浪前
今天给大家讲解的是创建线程以及线程的属性
祝愿所有点赞关注的人,身体健康,一夜暴富,升职加薪迎娶白富美!!!
点我领取迎娶白富美大礼包
🍓多线程编程:
前言:
我们为什么不用多进程?
多进程相关的API在Java标准库中没有提供
Java适合使用多线程来进行编程:
多线程在并发编程的时候,效率更高
尤其对于Java进程是要启动Java虚拟机来说
启动虚拟机这个事情开销很大, 搞多个Java进程就是搞多个Java虚拟机
于是java使用了标准库把与多线程编程有关API给封装了
比如在Java中的Thread类
一个进程中至少有一个线程
这个进程中的第一个线程就叫做主线程
main方法就是主线程的入口方法
每个线程都是一个独立的执行流,相互独立执行,互不干扰
线程的执行顺序是不确定的,是随机的
为什么这里是随机的呢?
因为操作系统中有一个调度器模块,这个模块的实现方式就是类似于一种随机调度的效果
🍓随机调度:
一个线程什么时候被调度到CPU上执行,时机是不确定的
一个线程什么时候从CPU上下来,给别的线程让位, 时机也是不确定的
随机调度这种也叫抢占式执行:
这个特性会导致多线程安全问题
Windows等等主流的操作系统都是抢占式执行
🍓观看线程的详细信息:
使用一个程序来观看线程的详细信息
在jdk中的bin目录之下有一个jconsole.exe的程序
在使用这个jconsole.exe程序查看线程的详细信息之前要先确保两个点:
- 确保你的程序(线程)已经先跑起来了
- 有些需要使用管理员方式来运行
如图所示:
🍓sleep
线程中的while循环转得太快了, 使用sleep方法来休眠,使得循环转得慢一些,
sleep是Thread的静态方法, 属于Thread类
我们可以在线程中加入sleep 来降低循环速度
时间单位换算:
1s = 1000ms
1 ms = 1000us
1us = 1000ns
🍓创建线程
线程的创建有好几种方式:
- 继承Thread类, 重写run方法
- 实现Runnable接口
- 还是继承Thread,重写run,但是使用匿名内部类
- 还是实现那Runnable, 重写run, 也是使用匿名内部类
- 基于Lambda表达式)最推荐的方式
🍓第一种: 继承Thread类, 重写run方法:
class MyThread extends Thread{ @Override public void run(){ while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
} public class ThreadDemo01 { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); while (true) { System.out.println("hello main"); Thread.sleep(1000); } }
}
结果如图所示:
🍓第二种:实现Runnable接口
class MyRunnable implements Runnable{ @Override public void run(){ System.out.println("hello Runnable"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }
} public class Text2 { public static void main(String[] args) throws InterruptedException { Thread t2 = new Thread(new MyRunnable()); t2.start(); while(true){ System.out.println("hello main"); Thread.sleep(2000); } }
}
结果如图所示:
🍓注意事项:
我们使用Runnable接口的方式和直接继承Thread类的方式有什么区别吗?
我们使用Runnable接口的方式有利于我们进行解耦合
🍓解耦合:
那什么是解耦合?
我们在创建一个线程,是需要两个关键操作的:
- 明确线程要执行的任务
- 调用系统的API创建出线程
那么此时若我们使用的是Runable接口
那么我们就可以把任务单独提取出来,提取出来之后
就可以随时把代码改成使用其他方式来执行这个任务
举个例子:
现在有一个一家三口,父亲,母亲,和儿子,现在家里面没有酱油了,此时就需要去执行“买酱油”这个任务
那么我们就可以把“买酱油”这个任务单独提取出来,之后这个任务是交给父亲执行,还是母亲执行,还是儿子执行都是没有本质区别的
而在代码中就是把这个任务单独提取成Runnable, 后续是谁来执行都可以进行轻松的调整
这个就是解耦合
🍓Runnable
Runnable可以理解为可执行的
作用:
通过这个接口就可以抽象表示出一段可以被其他实体来执行的代码~~
在代码中的run方法就是这个Runnable要表示的一段代码
class MyRunnable implements Runnable{ @Override public void run(){ System.out.println("hello Runnable"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }
}
但是这个Runnable只是一段可以执行的代码,还是要搭配Thread类,才能够真正地在系统中创建出线程
就是把线程和要执行的任务进行了解耦合:
如下所示:
Thread t = new Thread(new MyRunnable());
🍓第三种 : 继承Thread类, 重写run方法,使用匿名内部类:
匿名内部类 :
在一个类中定义的类,没有名字, 也就不能够重复使用,用一次就扔了
这个匿名内部类是Thread的子类, 同时又把这个匿名内部类的实例给创建出来
而且这个匿名内部类是可以重写run方法的
public class Demo3 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run(){ while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t.start(); while(true){ System.out.println("hello main"); Thread.sleep(1000); } }
}
🍓第四种: 实现Runnable接口, 重写run, 使用匿名内部类
public class Demo4 { public static void main(String[] args) throws InterruptedException { Runnable runnable = new Runnable() { @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread t = new Thread(runnable); t.start(); while(true){ System.out.println("hello main"); Thread.sleep(1000); } }
}
🍓第五种: 基于Lambda表达式(最推荐的方式)
Lambda表达式是更简洁的语法表示方式: (语法糖)
以下就是一个for循环的语法糖
for(int x : arr)
Lambda表达式如下:
Thread t= new Thread(() -> {
});
Lambda表达式:
Lambda表达式是一个匿名函数, (无名函数; 一次性的)
主要是用来实现回调函数的效果的
回调函数:
回调函数 :
不是程序员主动调用, 也不是现在立即调用
而是把调用的机会交给别人(操作系统, 库, 框架, 别人写的代码)
交给别人之后在合适的时机来进行调用
Lambda表达式的本质:
Lambda表达式本质上就是一个函数式接口,通过函数式接口来描述一个方法,本质上还是没有脱离类
public class Demo5 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); while(true){ System.out.println("hello main"); Thread.sleep(1000); } }
}
🍓线程的其他属性:
构造方法:
Thread(String name) :
name不会影响到线程的执行, 就只是给线程取不同的名字;为了方便调用和调试
而且线程间的名字是可以重复的,但是要起一个有意义的名字
例子:
创建一个线程, 命名为 " 这个是一个新线程"
public class Demo5 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } },"这个是新线程"); t.start(); while(true){ System.out.println("hello main"); Thread.sleep(1000); } }
}
getId() :
JVM自动分配的身份标识,会保证唯一性, 标识一个进程中唯一的一个线程
这个ID是java给你这个线程分配的ID, 不是系统API给你分配的, 也不是PCB中的ID
getState() :
线程的状态,显示线程是就绪状态还是阻塞状态)
getPriority() :
线程的优先级: 由于系统是随机调度的方式
在java中设置优先级效果不明显;只是对内核调度器的调度过程产生了一些影响:
isDaemon():
描述当前线程是否是守护线程:(后台线程)
t.setDaemon(true); //设置为后台线程
不写这个代码就默认是前台线程
🍓前台线程与后台线程的区别:
后台线程: 后台线程运行不会阻止进程结束
前台线程: 前台线程运行会阻止进程结束
我们创建的代码默认是前台线程,在运行过程中会阻止进程结束
只要前台进程没有执行完毕,那么进程就不会结束
即使main方法已经执行完毕了,进程也不会结束
如下图所示:
isAlive() :
表示内核中的线程(PCB)是否存在
java代码中定义的线程对象(Thread) 实例, 虽然表示一个线程
但是这个对象本身的生命周期和内核中PCB的生命周期不完全一样
Thread t = new Thread()
此时t对象有了,但是内核PCB还没有,isAlive 就是false
当执行了下面的代码之后,才创建了PCB:
t.start();
此时才有了内核PCB, 才真正的在内核中创建了这个PCB, 此时isAlive() 就是true
start()
Thread类使用start方法来启动一个线程
对于同一个Thread类来说, start()方法只能够调用一次
🍓start() 和 run() 方法的区别
有一个经典的面试题:
start() 和 run() 方法的区别是什么?
如果是调用run()方法来执行, 那么就没有创建新的线程
在代码中只有main这个主线程, 在代码中只有一个线程,
此时这个主线程就只能够停留在run方法中的循环里面, 一直打印hello thread
不会去执行mian方法中下方的while循环的代码,也就不会打印hellow main
如图:
那如果是调用的start() 方法来执行代码,则会创建一个新的线程
而这个新的线程就会去执行run()方法中的循环, 来打印hellow thread
而main方法中的主线程就会去继续向下执行下方的while循环中的代码了
也就会在循环中不断地打印hellow main , 此时是有两个线程同时执行的,
第一个是通过start()创建出来的新线程 在打印run()方法中代码
第二个是main主线程在执行while循环中的代码
如图:
🍓总结:
两者的区别如下:
start方法的内部是调用系统的API
在系统内核中创建出线程,然后再由这个线程去执行run方法
run方法是单纯描述了这个线程的要执行什么内容
这个run方法会在start方法创建好线程之后自动被调用
没有创建出新的线程, 只是一个run方法