1. 认识线程(Thread)
1.1 关于线程
1.1.1 线程是什么
由前一节的内容可知,进程在进行频繁的创建和销毁的时候,开销比较大(主要体现在资源的申请和释放上),线程就是为了解决上述产生的问题而提出的方案;线程保持了独立调度执行,这样的“并发支持”,如此同时省去“分配资源”“释放资源”带来的额外开销。
一个线程就是一个 "执行流". 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行 着多份代码, 一个进程中可以并发多个线程,每条线程并行执行不同的任务。
1.1.2 为啥要有线程
1、首先, "并发编程" 成为 "刚需".
单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源. 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编 程.
2、其次, 虽然多进程也能实现 ,并发编程, 但是线程比进程更轻量.
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.
3、最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程" (Coroutine)
1.2 进程与线程
1.2.1 简单讲解
下面来可能错误的分析一下进程与线程在创建和销毁的时候对内存地址的利用:
下图我们用pcb来描述一个进程,图解如下:
如上图所示,每一个进程在创建和销毁时都会在内存空间操作自己的地址,在进行多进程并发执行的时候,多个进程大规模的创建和销毁会再一定程度上消耗系统和操作系统的资源;
下图我们来用pcb描述一下线程,图解如下图所示:
pcb中有一个属性就是内存指针,如上图所示,多个线程的内存指针都指向内存地址中的同一个位置;
这就意味着以上多个线程只有在第一个线程创建的时候需要从系统中分配资源,后序的线程就不需要继续在分配资源了,直接公用前面的那份资源就行了;
同时除了内存之外,文件描述符(操作硬盘),也是多个线程共用一份的;当然我们也要注意不是所有的线程都能实现如上程度的资源共享的,只有我们设置的线程组才能实现资源共享;
综上:线程的出现解决了频繁申请和释放资源的开销
1.2.2 两者的关系
没有进程的时候,进程扮演两个角色(资源分配的基本单位和调度执行的基本单位)
引入了线程之后,进程只需要扮演一个角色(资源分配的基本单位),线程分担了一个角色的(调度执行的基本单位)
关于线程和进程的小结:
1、进程是包含线程的;
2、每一个线程也是一个独立的执行流,且可以执行一段代码,并且单独的参与到cpu调度中(状态,上下文,优先级,记账信息,每一个线程都有自己的一份)
3、每个进程有着自己独有的资源,进程中的线程公用这一份资源(内存空间和文件描述符)----->(进程是资源分配的基本单位,线程是调度执行的基本单位)
4、进程和进程之间,不会相互影响;如果一个进程的某个线程抛出异常,是可能会影响到其他线程的,由此会把整个进程中的所有线程都异常终止;
5、同一个进程中的线程之间,可能会相互干扰,从而引起线程安全问题;
6、当然线程不是越多越好,要能够合适,如果线程太多的话,调度开销就可能十分明显;
1.3 java线程和操作系统之间的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
所谓多线程编程:
写代码的时候可以使用多线程进行并发编程(在java中不太推荐,很多和多线程编程相关的api在java标准库中都没有提供),也可以使用多线程并发编程(系统提供了多线程编程的api,java的标准库把这些api封装了,如此在代码中就可以使用了)
由此可以得出多线程在并发编程的时候,效率更高(频繁创建销毁的时候),尤其是对于java进程,是要启动java虚拟机的,如果启动java虚拟机,则这个事情的开销更大,--->可类似的看成搞多个java进程就是多个java虚拟机。
2. 初识多线程程序
首先要有一个注意的点:
- 每个线程都是一个独立的执行流
- 多个线程之间是 “并发” 执行的
2.1 详细代码
代码一:通过代码详细了解thread类:
Java中提供的api,是通过thread这样的类进行展开的。
package thread;
// 1. 创建一个自己的类, 继承自这个 Thread
class MyThread extends Thread {
@Override
public void run() {
// run 方法就是该线程的入口方法.
System.out.println("圣诞节快乐,委婉待续!!!");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// 2. 根据刚才的类, 创建出示例. (线程实例化, 才是真正的线程).
// MyThread t = new MyThread();
Thread t = new MyThread();
// 3. 调用 Thread 的 start 方法, 才会真正调用系统 api, 在系统内核中创建出线程.
t.start();
}
}
结果如下:
代码二:编写mythread自定义类线程,在主线程中运行该自定义线程,代码如下:
package thread;
//1、创建一个自己的类,继承自这个thresd
class MyThread2 extends Thread{
//重写run方法
@Override
public void run() {
//run 方法就是该线程的入口方法
while (true){
System.out.println("hello thread,委婉待续");
try {
Thread.sleep(1000);//休眠1000ms
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
//创建一个main方法
public static void main(String[] args){
//2、根据刚才的类,创建出实例(线程实例,才是整整的线程)
MyThread2 t = new MyThread2();
//3、调用thread的start方法,才会整整的调用系统的api,在系统的内核中创建出线程池,
//然后线程就开始运行我们的run方法中的代码
t.start();
while (true){
System.out.println("hello main,smallye");
try {
Thread.sleep(1000);//休眠1000ms
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
结果显示:
结果显示交替进行,这就展现出了多线程与普通程序的区别
2.2 代码深入分析:
2.2.1 一些问答
Q1:关于上图Thread类为啥能直接使用,而不需要进行导包?
A1:在java标准库中,有一个特殊的包,java.lang(这个包时默认被导的,这里面的类默认使用)
Q2:为啥我们自定义类mythread2前面没有public?可以加public吗?
A2:不能,一个.java文件中,只能有一个public的类,这个mythread类没有被public修饰,就是只能在当前包里被其他的类使用。
Q3:讲解一下run方法与main方法的区别?
A3:上图就类似于main方法,该run方法是一个java进程(程序)的入口方法(一般将“跑起来”的程序称为进程,没有运行起来的程序(.exe),称为“可执行文件”),且此处的run方法,不需要手动调用,会在合适的时候(此时是线程创建好了之后,即被实例化后),被jvm自动调用执行。(如此风格的函数,称为“回调函数callback”)
Q4:以上字符代表的含义?关于方法重写的含义?
A4:
1、首先上图字符是方法重写的注解,主要目的就是方便让编译器检查我们的代码是否构成方法重写(语法中有很多机制,就是让编译器对我们的代码进行检查,如果我们明确该方法是重写的,有了这个注解编译器就会检查我们的方法是否满足方法的重写,参数等是否满足方法重写的要求,这样就能够及时的报错,大大的提高了我们的工作效率)
2、方法重写:就是让你能够对现有的类,进行扩展,写出符合场景需求的具体方法。
我们写的以上线程,肯定是让这个线程执行一些代码的。Thread类本身就会带有一个人入口方法,但是很明显标准库自带的run是不知道我们的需求业务是啥样的,所以我们必须要手动指定(即写出一个具体的业务),这样就需要针对原有的Thread进行扩展,Thread会有很多属性方法,大部分内容复用即可,只要把需要扩展的内容进行扩展即可。
Q5:thread.sleep的作用?
A5:所谓sleep是java中封装后版本中thread中提供的静态方法,其主要作用就是让当前的线程进行休眠,时间单位是ms;
当然sleep会出现java.lang.interruptedException异常,该异常出现的原因是我们要求休眠1000ms的线程会由于其他原因导致提前被唤醒,不能够休眠1000ms
Q5.1:下图中两个线程中的sleep为什么只有前者可以进行try-catch捕捉,而后者既可以进行try-catch捕捉,也可以进行抛出方法签名?
A5.1:正常情况下,一般受查异常既可以添加异常方法签名也可以使用try-catch捕捉;但是我们的前者sleep所在的mythread类中的run方法是要经过重写操作是具体实现的方法,如果我们添加了方法签名,那么由于语法所致,会让我们的方法不能构成重写操作;更有父类thread的run没有throws异常,所以子类重写的时候也不能有throws异常;
2.2.2 多线程的运行逻辑
如上图所示:
在main方法中,主线程调用start方法(创建了t线程),此时cpu的两个核心开始运转,兵分两路,一方面执行沿着mian方法继续执行,即打印“hello main,smallye”;另外一方面,内核就通过刚才主线程api构造出t线程,并且执行run方法,即打印“hell0 thread,委婉待续”;同时这两个线程在同时执行的时候各论各的,互不干扰。
但是正因为这样,所以考虑两个线程的执行顺序是一样的吗?
其实这两个线程的执行顺序是不一样的,因为在操作系统的内核中,有“调度器”模块,该模块实现的方式是一种类似于“随机调度的”效果;
所谓随机调度会导致以下两个后果:
1、一个线程,被调到cpu上执行的时机是不确定的;
2、上位到cpu里被执行的线程从cpu上下来的给别的线程上位的时机也是不确定的,如此就会导致线程“抢占式执行”,且当前的主流操作系统都是抢占式执行的;
由于此案成创建本身是有开销的,故此在该开销本身的影响下,导致“hello main,smallye”会比“hell0 thread,委婉待续”快一点(大概率,但是不一定),综合题前所学,进程创建第一线程的时候开销是最大的,剩下的线程的开销都计较少;
2.3 使用 jconsole 命令观察线程
我们可以使用jdk自带的工具 jconsole查看当前Java进程中所有的线程
操作流程如下:
1、第一步,找到jdk
2、第二步,点进去,找到里面的bin文件点进去
3、第三步,点击bin文件夹里面的jconsole
4、第四步,找到你所创建进程
5、第五步,直接点击不安全连接就好
6、第六步,点击线程进行查看
我们的t是指线程的变量名,所谓的看到的thread—0,是我们自定义线程的默认名字,一般会从0~n;
ps:本次的内容就到这里了,如果感兴趣的话就请一键三连哦!!!