个人主页:兜里有颗棉花糖
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创
收录于专栏【Java系列专栏】
本专栏旨在分享学习Java的一点学习心得,欢迎大家在评论区交流讨论💌
目录
- 一、背景引入
- 二、线程的引入
- 小总结
- 三、进程和线程的关系(面试题)
- 四、第一个多线程程序
- 每个线程都能够被独立的进行调用执行
一、背景引入
在引入多线程之前,我们先来看一下进程是为了干什么的,一句话总结就是:满足"并发编程
"这样的需求。
早些时候,CPU都是单核心的CPU,但是随着技术工艺变得越来越强,单核心CPU的性能的确是越来越强的,但是单核心CPU技术工艺研发到一定程度后就会遇到瓶颈,从而导致单核心CPU的研发变得非常缓慢。于是就就逐渐向多核心CPU的方向进行发展研究。
好了,既然CPU逐渐变成了多核心,那么应用程序也应该做出相应的调整来把多核心的CPU给利用起来(意思就是仅仅由硬件上的支撑是不够的,我们还要由软件上的配合),以免造成一核有难,多核围观的场景。
另外,早期的系统一般是单任务系统,即在同一时刻只能使用一个程序,而无法在同一时刻内使用多个程序,完成多个任务。而要完成多个任务的话,进程就显得特别关键。
二、线程的引入
多进程能够很好的实现并发编程的效果,但还由一个缺点就是进程太重,即消耗资源更多,速度更慢。如果进程的创建和销毁比较少的话那还勉强可以,但是如果进程的创建和销毁非常频繁的话,此时就会产生非常多的开销。开销大主要体现在需要给进程分配资源,而给进程分配资源的这一过程也是需要很多时间成本的。
于是就有人提出这样一个解决方案:即在创建进程的过程中,只分配简单的PCB(进程控制块),而不去分配后续需要的内存硬盘资源,即把分配资源的时间省去,这样既可以完成多并发编程的任务又能够提升进程创建销毁的速度。
这样一种只分配简单的PCB而不去分配后续需要的内存硬盘资源的这样一种方式称为轻量级进程
,也叫做线程
(thread
)。所以简单来说线程就是只分配PCB,但不分配后续需要的内存硬盘资源
。
但是线程总归是要完成任务的,如果不去分配后续需要的内存硬盘资源显然是不可以的,于是就有人想出来这样的一个办法:在创建进程的时候,先把后续需要的内存资源分配好;后续创建线程的时候让线程在进程内部直接复用先前进程分配创建好的内存资源(线程和进程直接的关系:进程包含线程)
另外,一个进程至少要包含一个线程,最初进程创建出来的时候,我们可以将这个进程视为一个只包含一个线程的进程(注意此时创建进程的过程是需要分配资源的,同时第一个线程的创建开销是可能是比较大的
),我们后续再次创建线程的时候就不需要分配资源了,也就省去了分配资源的过程,因为资源已经是有了的。
小总结
好了,我们已经知道多进程已经可以完成并发编程的任务了,但是由于进程本身有些重,重就体现在进程的创建和销毁所需要的内存硬盘资源开销有些大。
所以就引入了线程(也称为轻量级进程
)来更高的解决上述的问题。进程中的多个线程公共复用了进程中的各种内存硬盘等资源,同时这些进程可以各自独立的在CPU中进行调度。所以这样的话线程既可以完成并发编程的效果,又可以以轻量级的方式来完成各种任务。
另外,这里补充一个点,进程和线程是两个不同的概念,在Linux中复用了PCB这个结构体来描述线程,而在Windows中,描述进程和线程是不同的结构体。所以在Windows中,为了更好的描述进程,有时一个PCB对应一个线程,多个PCB对应一个进程(即可能是多一个PCB来描述一个进程,一个PCB来描述一个线程)。
CPU的内存指针和文件描述符表文件描述符表这两个字段内容在同一个进程是一样的,但是上下文信息、状态、优先级、记账信息......等支持调度的属性
,这些PCB就不一样了(意思就是每个线程独立去CPU上进行调度,每个线程的上下文、状态、优先级、记账信息等支持调度的属性都有自已独立的一份
)。
线程的引入的确能够提高任务执行的速度,同时随着线程数量的增多任务执行完成的速度当然也会增快,但是线程的数量不能是无限制提高的(CPU的核心数的数量是有限的。),同时线程数量也不是越多越好,因为线程调度的开销有时会拖慢整个程序的效率。
两句话总结:
1.进程是操作系统中进行资源分配的基本单位。
2.线程是操作系统中进程调度执行的基本单位。
三、进程和线程的关系(面试题)
1.进程包含线程,都是实现并发编程的一种方式,但是由于多进程的创建和销毁的效率并没有那么高,且线程比进程更加轻量。所以就引入了多线程的概念。
2.进程是系统分派资源的基本单位;线程是是系统调度的基本单位,创建进程的时候已经把分配资源(
这里的资源主要指的是虚拟地址空间和符表
)的工作做了,后续创建进程的时候直接复用之前的资源即可。
3.每个进程都有自己独立的地址空间,彼此直接不会相会影响到,从而保证了每个进程的独立性,保证了数据稳定性。
需要注意的是,如果多个组线程程共用了一份地址空间的话,一旦某个线程抛出异常的话,就可能会导致一整个进程结束(所以多个线程之间是很容易相互影响的。 )。
四、第一个多线程程序
线程本身是操作系统的概念,而操作系统又提供了API来供我们使用线程,来完成多线程编程(在Linux中称为pthread库
,通过这个库我们就可以完成多线程编程)。
而在Java中,把操作系统中的API进程分装,即提供了Thread类
来帮助哦我们来完成多线程编程。
Java标准库中的Thread类来自于java.lang这个包
,而Java.lang这个包
不需要我们手动的import
。
好了,光说不练是不行的,接下来我们就来看一下我们的第一个线程程序,请看下图:
package thread;
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello,world!");
}
}
public class Demo01 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
现在我们来解释上述的线程程序,上述代码的MyThread类
中的run()方法
是重写的Thread父类
中的run()方法
,这个run()方法
相当于线程程序的入口,线程程序跑起来之后具体要干什么事情,都是通过这个run()方法
来进行描述的。
myThread.start()
这个操作就是在创建一个线程(即在底层调用操作系统提供的创建线程的API,同时在操作系统内核中创建出其对应的PCB结构,并将其加入到链表中)。此时这个新创建出来的线程就会参与到cpu的调度中,接下来线程要执行的任务,就是run()方法
中的内容。
接下来我如果注释掉
myThread.start();
,即直接执行MyThread类中的run()方法
,请看下图:
我们来解释上述程序,我们前面已经知道,start方法
会去调用操作系统提供的创建线程的API,同时在操作系统内核中创建出其所对应的PCB,此时这个新创建出来的线程就会参与cpu调度
。
而我们直接运行run方法的话,此时并没有调用操作系统提供的创建线程的API,也就没有创建出新的进程。
当整个程序开始运行的时候,此时会先创建出来一个java进程,这个java进程中至少包含一个进程,称为主进程,这个主进程是负责执行main()方法的线程。
每一个线程都能够被独立的进行调用和执行。
每个线程都能够被独立的进行调用执行
每个线程都能够被独立的进行调用执行
,现在我们来写一个程序来体现这句话。请看:
运行结果如下(由于程序是一个死循环,所以这里只是截取了部分的程序运行结果),如下图:
我们可以看到程序一会执行Hello,Linux!
一会执行Hello,world!
解释:start()是新线程执行的入口,而主线程和新线程是并发执行的一个关系。
先新创建出来的线程创建出来之后会执行run()方法中的代码,而main()方法则是继续往后执行。所以最终呈现出来的程序运行效果就是一会执行Hello,Linux!
,一会执行Hello,world!
。
现在我们在来看一段程序,请看下图:
我们可以看到上述代码把myThread.start()
注释掉了,相当于我们并没有创建线程,那么此时的话只有整个程序只有主线程这一个线程了。
分析到这里我们已经知道整个程序只有线程(也就是主线程),那么执行的run()方法
就是在主线程中去执行的,因为整个程序就只有一个线程(即主线程)。
总结一下:
- 每个线程都是一个独立的执行流。
- 每个线程都可以执行一段代码。
- 每个线程之间是并发的关系。
好了,本文到这里就结束了,就到这里吧,再见啦友友们!!!