JavaEE基础 ——— 关于线程的那些事
目录
变形3:lambda 表达式创建 Runnable 子类对象
一、什么是进程
每个应用程序运行于现代操作系统之上时,操作系统会提供一种抽象,好像系统上只有这个程序在运行,所有的硬件资源都被这个程序在使用。这种假象是通过抽象了一个进程的概念来完成的,进程可以说是计算机科学中最重要和最成功的概念之一。
进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做程序的一次运行过程;同时,在操作系统内部,进程是操作系统进行资源分配的基本单位。
二、进程的调度
1.描述一个进程
明确出一个进程上面的一些相关属性。就比如:形容一个学校的学生,你需要学校姓名,班级、专业等。
2.组织若干个进程
使用一些数据结构(例如链表),把很多描述进程的信息放在一起,方便进行增删改查。
较为典型的就是使用双向链表来把每个进程的PCB给串起来。因为操作系统的类型的不同,内部实现不尽相同。因此我们以 linux 系统为例进行讨论。linux组织进程的方式就是用双向链表来把每个进程的PCB给串起来。或者说,所谓的“创建进程”,就是先创建出 PCB,然后把 PCB 添加到双向链表中,所谓的“销毁进程”,就是找到链表上的某个PCB,将它从链表上移除。而“查看任务管理器”,就是遍历链表,找到我们想要的那一个PCB后,取出相关资料。
有了上述这些基础之后,我们就可以来聊一聊进程是怎么调度的了。
打开任务管理器,我们会发现我们的计算机上有许多任务在正在运行,那么我们的CPU又是如何管理运行他们的?
3.并发和并行
我们首先来明确两个概念:并发和并行
并发:微观上看,假设现在有ABCD四个任务,而我们的CPU并不是让这四个任务同时运行的,CPU先是运行一会A,再运行一会B再运行一会C.......那么循环运行的同时,CPU又在ABCD四个任务之间切换地非常快,那么我们从宏观的角度去看就好像是让ABCD四个任务在同时运行,这就是并发。
并发:单个核心 按照串行的方式 执行多个任务,但是只要它切换的足够快,从宏观来看就好像是多个任务在同时执行一样。
并行:现在有ABCD四个任务,我们的CPU让这四个任务同时运行,这就是并行。
并行:多个核心执行多个任务
从我们人的角度其实是区分不了并发和并行的,因此我们在写代码的时候也不去具体区分这两个词。我们平时常说的并发编程就是包含了 “并发” 和 “并行”的统称。
到这里我们就知道,所谓的进程调度就是让有限的CPU核数,去执行更多的任务。
4.进程的状态
1、就绪状态
2、阻塞状态 / 睡眠状态,暂时不去CPU 上调度执行
5.优先级
我们计算机上有很多的进程,那么先给谁分配资源,后给谁分配资源;以及给谁分的多,给谁分的少。这一点,操作系统给进程分配时间也是类似的。有的进程优先级高,有的进程优先级低。
6.记账信息
CPU可以根据记账信息来灵活调整调整它的一个资源的分配策略。先统计好这里面每个进程当前所吃的资源,然后再根据这里面的统计结果,来去对那些分配特别少,特别不均衡的进程,做出一些补偿。
7.虚拟地址空间
由于某个进程出现了bug,导致该进程崩溃了,那么是否会影响到其它进程呢?
事实上某个单个进程发生崩溃,其它进程是不会受到影响的。这就是“进程的独立性”,而做到各个进程不相互干扰的功臣就是虚拟地址空间
拿我们身边的一个例子来说:疫情期间假如有一栋楼被封锁了,假设这栋楼外部只有一条路能走,那么如果出现了一例阳性(进程崩溃)那么所有走这条路的人都有被感染的风险。
那么我们在不考虑实际的情况下,假设楼里一共住着20个人,那么我们在楼外开辟20条互不干扰的道路,每一个人都有一条独立的路。那么,在这种情况下,如果二十人当中出现一例阳性,那么其他人感染的风险也会大大下降。
8.进程之间的通信
但现代的应用,要完成一个复杂的业务需求,往往无法通过一个进程独立完成,总是需要进程和进程进行配合地达到应用的目的,如此,进程之间就需要有进行“信息交换“的需求。进程间通信的需求就应运而生
通过“公共空间”进行交互。(进程A 先将数据放入公共空间,进程B随后再去取,这样就完成了进程之间的交互)
三.线程与进程
1. 线程是什么?
一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码。
为了方便理解我们可以举这样一个例子:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
2.线程的作用
1.单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源。
2.有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程。
3.进程和线程的区别
1.进程是包含线程的,每一个进程至少有一个线程,即主线程
2.进程和进程之间不共享内存空间,而同一个进程的线程共享同一个内存空间
3.进程是系统分配资源的最小单位,线程是系统调度的最小单位
四.线程的创建
方法一:Thread类继承
class MyThread extends Thread {
@Override
public void run() {
System.out.println("方法一");//线程需要执行的任务
}
}
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();//创建实例
t.start();//启动线程
}
}
方法二:实现Runnable接口
class MyRun implements Runnable {
@Override
public void run() {
System.out.println("方法二");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRun());
t.start();
}
}
其他变形
变形1:匿名内部类创建Thread子类对象
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("变形1");
}
};
t.start();
}
变形2:匿名内部类创建Runnable子类对象
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("变形2");
}
});
t.start();
}
变形3:lambda 表达式创建 Runnable 子类对象
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("变形3");
});
t.start();
}
拓展:多线程的优势
我们用一个例子来看一下多线程并发执行和我们平时串行之间的差异:
代码一:串行执行一个数字自增一百亿次,记录其执行时间
代码二:并发执行实现一百亿次自增,记录执行时间
代码一:
public static void main(String[] args) {
long start = System.currentTimeMillis();
int num = 0;
for(int i = 0; i < 500000; i++) {
num++;
}
for(int i = 0; i < 500000; i++) {
num++;
}
System.out.println(num);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
执行时间:
代码二:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new MyThread2();
long start = System.currentTimeMillis();
t1.start();//启动线程一
Thread t2 = new MyThread2();
t2.start();//启动线程二
//保证线程全部完成
t1.join();
t2.join();
long end = System.currentTimeMillis();
System.out.println(MyThread2.num);
System.out.println("执行时间:" + (end - start) + "毫秒");
}
class MyThread2 extends Thread {
public static long num;
private static Object o = new Object();
@Override
public void run() {
synchronized (o) {
for(long i = 0; i < 50_0000_0000L; i++) {
num++;
}
}
}
}
从上述两个代码和执行结果我们可以看到:使用多线程之后程序的运行效率有了明显的提升,但是要注意的是:并不是一味地使用多线程就可以提高运行效率,创建线程也需要一定的时间,如果代码本身执行时间是低于创建线程时间的,那么使用多线程反而会起到反效果。
五.Thread类及其常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。每个执行流,也需要有一个对象来描述,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
常见的构造方法
方法 说明 Thread() 创建线程对象 Thread(Runnable target) 使用 Runnable 对象创建线程对象 Thread(String name) 创建线程对象,并命名 Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名 Thread(ThreadGroup group,
Runnable target)线程可以被用来分组管理,分好的组即为线程组,这
个目前我们了解即可
常见属性
1. ID :是线程的唯一标识,不同线程不会重复
2.名称:是各种调试工具用到
3.状态:表示线程当前所处的一个情况
4.优先级:优先级高的线程理论上来说更容易被调度到
5.后台线程:需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
6.是否存活: run 方法是否运行结束了
启动一个线程
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。覆写 run 方法是提供给线程要做的事情的指令清单。线程对象可以认为是把 李四、王五叫过来了,而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("任务");
});
t.start();
}
中断一个线程
方法 说明 public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,
否则设置标志位public static boolean
interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位 public boolean
isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位
等待一个线程
方法 说明 public void join() 等待线程结束 public void join(long millis) 等待线程结束,最多等 millis 毫秒 public void join(long millis, int nanos) 同理,但可以更高精度
获取当前现象的引用
方法 说明 public static Thread currentThread(); 返回当前线程对象的引用
休眠当前线程
方法 说明 public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis
毫秒public static void sleep(long millis, int nanos) throws
InterruptedException可以更高精度的休眠
以上就是今天的全部内容了,觉得有帮助的话就请点一个赞再走吧!