文章目录
一. 线程
并发执行, 是指为了利用多核CPU, 我们要把要完成的任务, 拆解成多个部分, 并且分别让他们在不同的CPU上运行, 那么此时进程会被调度到不同的CPU上执行
此时, 每个客户端连上服务器, 服务器都会创建一个进程, 客户端断开, 再把进程释放掉, 那么如果这个服务器, 频繁地有客户来来去去, 服务器也需要频繁地创建和销毁进程, 此时就会带来新问题, 创建和销毁进程的花销太大了!!!
此时就引用了线程(thread), 线程创建和销毁的开销更小
可以理解为线程是"进程的一部分", 一个进程, 可以包含一个线程, 或者多个线程
事实上吗更严格的来说, 一个PCB其实是描述一个线程的
- pid 每个线程都不一样
- 内存指针 同一个进程的若干个线程, 这里的内存指针其实是同一个
- 文件描述表 同一个进程的若干个线程, 这里的文件描述表其实是同一个
- 状态, 优先级, 上下文, 记账信息, 每个线程都有自己的属性
- tgid 同一个进程的tgid是同一个
同一个进程中的若干个线程之间, 是公用内存资源和文件资源的(线程1中new了一个对象, 线程2重视可以直接访问到的; 线程1中打开了一个文件, 线程2也是可以直接使用的), 但是每个线程都是独立在CPU上调度执行的
进程是系统资源分配的基本单位
线程是系统调度执行的基本单位
-
为什么说, 线程比进程更轻量?为啥说线程创建/销毁的开销, 比进程小?
-
核心就在于, 创建进程, 可能要包含多个线程, 这个过程, 涉及到资源分配/资源释放
而创建线程, 相当于资源已经有了, 省去了资源分配/释放的过程了
多线程编程值得关注的难点: 一个线程抛出异常, 没有很好的捕获处理好, 就会使整个进程退出(其他线程也就没了), 而相比之下, 独立性更好, 一个进程异常, 不会影响到其他进程
面试题: 线程和进程的区别?
二. 多线程代码
线程, 本身是操作系统提供的, 操作系统提供了api, 让我们可以操作线程, JVM就对操作系统进行了封装, 提供了Thread类
- 创建线程
- 重写run方法
run方法的作用, 就是描述了线程具体干什么活
- 启动线程
调用start方法
此时运行的结果为:
上述代码中其实有两个线程
- thread线程
- main方法所在的线程(主线程)
jvm进程启动的时候, 自己创建的线程
我们把代码修改一下:
在run中和main中分别加入两个死循环, 运行程序, 让进程一直执行下去
我们通过java提供的工具, 能更清楚的看到代码中的线程
选择我们创建的进程, 并连接
选择线程
我们就可以看到:
Thread线程是我们手动创建的
main是执行main方法的线程
剩下的都是jvm帮我们做的一些其他工作涉及到的线程, 有的负责垃圾回收, 有的负责记录调试信息…
运行上述代码, 消耗了大量的CPU资源, 主要是因为while循环太快了!
sleep
这个方法, 可以让线程主动进入阻塞状态, 主动放弃CPU上执行
括号中的单位为毫秒 1000ms => 1s
时间到了之后, 线程才会解除阻塞状态, 重新被调度到CPU上执行
把鼠标放在sleep上发现会抛出受查异常, 需要我们try-catch一下
由于打印操作消耗的时间非常短, 相比于sleep1s, 可以忽略不计了, 因此加上sleep可以使CPU消耗的资源大幅度降低了
未来实际开发中, 发现你负责的服务器程序, 消耗的CPU资源超出预期, 你如何排查这个问题?
- 首先需要确认, 是哪个线程消耗的CPU比较高(未来会涉及到一些第三方工具, 可以看到每个线程的CPU消耗情况)
- 确定了之后, 进一步排查, 线程中是否有类似"非常快速"的循环
- 确认清楚, 这里的循环是否应该这么快?
如果应该, 说明你们需要升级更好的CPU
如果不应该, 说明需要再循环中引入一些"等待"操作(不一定是sleep)
运行之后我们发现, 每秒钟会打印一次hello main 和 hello thread
两两一组我们发现, 打印的顺序不是固定的, 有可能main在前面有可能thread在前面
多个线程的调度顺序, 是"无序"
的, 在操作系统内部称为"抢占式运行"
任何一个线程, 在执行到任何一个代码的过程中, 都可能被其他线程抢占掉他的资源, 于是CPU就给别的线程执行了
这样的抢占式执行, 充满了随机性, 使多线程的程序的执行结果也难以预测, 甚至可能会引入bug
现在主流的操作系统(windows, linux…)都是抢占式执行
也有一些小众的系统(实时操作系统), 通过"协商式"进行调度, 如vxworks(风河 windriver公司)比较知名的实时操作系统
start
start是Thread里面自带的方法, 它的主要任务是:
调用操作系统提供的"创建线程"的api
在内核中创建对应的pcb, 并把pcb加入到链表中
当系统调度这个线程后, 就会执行上述run方法中的逻辑
注意: run不是start调用的, run是start创建的线程, 在线程里被调用的
如果我们调用run方法执行:
此时, 并没有创建新的线程, 就是在主线程中执行上述run中的循环, 此时执行的结果:
此时run和主线程的循环, 是串行执行的, 不是并发执行, 所以只会打印hello thread, 只有当run中循环结束, 才会执行main循环
像run方法这样, 只是定义好, 而不去手动调用, 把这个方法的调用, 交给 系统 / 其他的库 / 其他框架(别人) , 这样的方法称为"回调函数"(callback function)
创建线程的写法
1. 创建Thread子类, 重写run方法
就是上述我们用的方法
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 threadDemo1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2. 通过实现Runnable接口创建线程
创建NyRunnable类实现Runnable接口, 重写run方法
将MyRunnable对象作为参数传给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 threadDemo2 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
Runnable的任务是描述一个任务, run是要执行的任务本身
刚刚的第一种写法, 是线程Thread自己记录任务
而第二种写法, Thread本身没有记录任务, 由别人Runnable来记录的
引入Runnable的目的, 其实就是为了解耦合!
把任务内容和线程拆分开, 这样的任务, 就可以给其他地方执行, 当前是通过多线程的方式执行的, 未来也可以方便改成基于线程池方式执行, 也可以改成基于虚拟线程的方式执行(改动过程很简单)
3. 对于Thread, 使用匿名内部类实现
匿名内部类是java中的特殊语法, 这个类没有名字, 是在别的类中定义的
这里并不是new Thread, 此处是几个操作合并在一起了
- 创建了一个Thread子类(不知道名字, 匿名)
- 同时创建了一个该子类的实例,
对于匿名内部类来说, 只能创建这一个实例, 这个实例创建完了之后, 再也拿不到这个匿名内部类了 - 此处的子类内部重写了run方法
public class threadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
thread.start();
}
}
4. 对于Runnable, 使用匿名内部类实现
这里并不是new Runnable, 此处是几个操作合并在一起了
- 创建了一个Runnable子类(不知道名字, 匿名)
- 同时创建了一个该子类的实例
- 重写了run方法
public class threadDemo4 {
public static void main(String[] args) {
Thread thread = 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);
}
}
}
});
thread.start();
}
}
5. 使用lambda表达式
lambda表达式介绍
lambda本质上就是针对匿名内部类的平替
这里的lambda就是代替需要重写的run方法
public class threadDemo5 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
}
面试题: java中创建线程都有哪些写法?
补充:
1.catch语句中的代码怎么写?
这是编译器自动生成的, 继续刨除新的异常
但是在实际开发中, 是要自定义的,可能会打印一些日志, 把出现的异常的详情都记录到日志文件中; 可能触发一些重试类操作; 可能触发一些"回滚"类操作…
2.在main方法中, 处理sleep有两种选择1)throws 2)try-catch
但是在线程run中只有一种选择try-catch, 为什么?
其实throws也是方法签名的一部分
方法签名包括:
1)方法名字
2) 方法的参数列表
3) 声明抛出的异常
不包括返回值和public…
那么在方法重写时, 就要求方法签名得是一样的
而父类的run方法中, 也就是Thread类中run是没有抛出异常的, 我们也改不了, 所以子类也不能抛出异常