Java|Java 从多线程到并发编程(一)——初识线程 进程 多线程 Thread Runnable 匿名内部类


文章目录

    • 前言 ′???`
    • 进程 一段静态程序 动态执行的过程
    • 进程的切换
    • 进程的状态
    • 进程与线程
    • 多进程 VS 多线程
    • 多进程的应用
    • 线程的应用 实现之一 Thread
    • Thread实现图片下载
    • Thead的改进 匿名内部类
    • Thread = 线程?
    • 总结 ′?`

前言 ′???` 线程 进程 是操作系统中一个关键部分,无论是实践的项目还是你去面试找工作,都会涉及,为基础的重中之重。面试可以通过考察多线程技术,探你操作系统的学习深度,而平常实践中,线程是否安全,高并发的解决等问题也很实际
不扯那么多,千里之行始于足下,我们一起慢慢打好基础,学习知识,掌握思维,融会贯通,格物致知。
进程 一段静态程序 动态执行的过程
定义 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
说白了 进程(process)本质上是正在执行的一个程序,是容纳运行一个程序所需要所有信息的容器。所以这个容器里面存着代码、数据、堆栈(学过微机原理应该不陌生)
我们程序能够成为进程,主要经历有以下过程:
  • 静态的代码经过编译(或者解释),
  • 转为机器码
  • 操作系统分配系统资源来运行程序,比如内存空间
  • 也就是把机器码(可运行代码)放到内存里面
  • 除了存可运行代码 很重要的是保存运行结果 因此很多值处在内存的堆栈里边 并不和代码放一起
  • 如果得到CPU的青睐 这个内存的程序就有幸能够在CPU中 被CPU计算
  • 之前所说的堆栈中的初始值也会被运输到CPU的寄存器中 进行相应计算
  • 如果CPU另寻新欢 想抛弃这个进程寻求下一个进程的爱(好吧就是为下一个进程计算)
  • 那CPU也会负责任的把已经计算的结果 放到刚刚进程(前男友?)空间的内存堆栈里边
一个计算机系统进程包括(或者说“拥有”)下列数据:
程序的可运行机器码在存储器的映像
分配到的存储器(通常包括虚拟内存的一个区域)
存储器的内容包括可运行代码
特定于进程的数据(输入、输出)
调用堆栈、堆栈(用于保存运行时运数中途产生的数据)
分配给该进程的资源的操作系统描述符,诸如文件描述符(Unix术语)或文件句柄(Windows)、数据源和数据终端。
安全特性,诸如进程拥有者和进程的权限集(可以容许的操作)
处理器状态(内文),诸如寄存器内容、物理存储器寻址等。当进程正在运行时,状态通常储存在寄存器,其他情况在存储器。
可能你还是不太理解另寻新欢的过程,换言之,这就是进程的切换
进程的切换
进行进程切换 就是从正在运行的进程中收回处理器,然后再使待运行(所谓的就绪状态)进程来占用处理器。
换言之 其实可以理解为CPU决定要为哪个进程计算,就好像女神决定要为那个男士服务一样,如果这个男士不够格,也就是进程的优先级不够 那么CPU女王就会另寻新欢~ 之前处于就绪态的进程(备胎)就会上位。(当然这里 并非CPU做的决策 CPU只能计算 还有别的部分负责鉴定进程优先级 这里你可以这样yy理解)
所以之前的进程(前男友),存放在处理器的寄存器中的中间数据(主要是用于计算的)该怎么办?因为处理器的寄存器要腾出来让其他进程使用(让给下一位进程男士~)。那么被中止运行进程的中间数据存在何处?前男友进程的私有堆栈。
当然 除了用于计算的中间数据 还有很多进程状态的信息呢?
首先,进程间的切换是通过中断(interrupt)来实现的,因此,OS维护一张中断向量表,也被称为进程表,每个进程占用一个进程表项(进程控制块 Processing Control Block, PCB),该表项包含了进程状态的重要信息,包括程序计数器(保存了下一条指令的内存地址)、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,简而言之囊括了所有进程状态信息。
总之 进程男士,占用处理器女神,第一步就是把进程存放在私有堆栈中的数据,也就是前一次进程男士被中止(被抛弃)时的中间数据,再插入(复制移动MOV)到处理器女神的寄存器中去,同时查进程表,恢复之前的进程状态,
尤其是上次待运行 但没来得及运行的进程的断点(代码指针) 也就是上次没计算完 没做完的标记 送入处理器的程序指针PC,于是处理器开始接着上次的部分运行。
因此 切换包含了清理旧进程(该拿走的赶紧拿走 本CPU不存)以及接纳新进程(把数据和PC指针都注入进来吧~),换言之,也就是女神抛弃前男友和接入新男友两个部分
进程的状态 进程执行时的间断性,决定了进程可能具有多种状态。就类似男士是单身,还是已经有女朋友,还是被暂时抛弃,这几种情感状态一样~
1)就绪状态(Ready)—— 单身
进程已获得除处理器外的所需资源,等待处理器女神;只要分配了处理器,进程就可执行。
注意 就绪进程具有不同的优先级,会形成队列,就类似女神会给她的备胎们排序,对于进程而言,当一个进程由于时间片用完而进入就绪状态时,就会排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,则会排入高优先级队列。
2)运行状态(Running)——已有女朋友
进程占用处理器资源;
当然 如果 在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程 类似CPU会自己干自己的2333。
3)阻塞状态(Blocked)——不举
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行,因为还缺少其他必要条件。所以,“CPU觉得和他生活在一起太无聊”,另寻新欢,切换到别的进程。
进程与线程 这里可以参考阮一峰大佬(竟然与大佬同姓)的文章,讲比较生动有趣
多进程 VS 多线程 首先,进程起初是具有两项功能的——独立分配资源 与 被调度分派执行。既然能够被调度与分派执行,自然有多进程一说
但问题是,效率太低,试想,进程是具有独立系统资源的,比如一大块内存,如果频繁调度进程,会造成很多内存碎片——无论是扩容还是缩减,相对的,如果我们在进程下边还设计一个线程,线程来被调度,而且一个进程下边的线程共享进程资源,那么调度起来系统的开销就会小很多
因此后面,进程仅仅作为系统资源分配的独立单位,而把调度执行任务赋予线程。正如刚刚说说,进程不建议被频繁地切换,频繁的开辟内存空间,又频繁的更改中断进程表,因为系统维护起来很蛋疼。而线程作为系统调度和分派的基本单位,使用进程共享的内存资源,能轻装运行,会被频繁地调度和切换。
当然后面学到线程池的时候我们会清楚,即便是线程,我们系统为了极致性能,也不考虑运行时真的频繁创建销毁线程,而是开辟所谓线程池,预先准备好很多线程 以备需要。
多进程的应用 虽然我们听的最多的是多线程,但并非没有采用多进程的案例,其实我们熟悉的负载均衡软件nginx就是采用单线程多进程的方式来工作的,原因,
一方面是,他属于IO密集型,而非计算密集型,多线程的性能提升效果不明显。
另一方面是为了高可用。我们或许不知道多线程也有弊端,很明显,多线程共享同一个进程资源,会造成隐患,即一个线程占用过多系统资源会导致整个进程的崩溃,因此,这并不可靠。而Nginx 要保证它的高可用 高可靠性,,当某一个第三方模块引发了一个地址空间导致的断错时 (eg: 地址越界), 会导致整个Nginx所有服务全部挂掉。
所以多进程不会有这个问题吗?没错,只要所有进程加起来的使用量不超过服务器的负荷,就没有太大问题。因此当采用多进程来实现时, 往往不会出现这个问题.
当然,计算密集型应用是最常见的,因此很多应用会采用多线程,相对的,多进程就提的很少了。
线程的应用 实现之一 Thread 我们有三种方式来实现线程
  • Thread继承
  • Runnable接口实现
  • Callable接口实现
先来聊聊Thread继承这种方式(基础)
我们直观感受多线程 跑个小demo就好:
Run函数与Start函数
经典 区别就是 run函数就是直接调用(传统执行方式) 而start则具有多线程的效果
对比一下,我们把叫喵 miao(类名为thread_first)作为一个线程,叫汪 wang作为主线程,然后测试如下代码, 在主线程运行的同时,使用thread_first.start() 代码如下:
package com.base.threadlearning; public class thread_first extends Thread{ @Override public void run(){ for (int i = 0; i < 20; i++) { System.out.println("miao"+i); } }public static void main(String[] args) { thread_first thread_first = new thread_first(); thread_first.start(); for (int i = 0; i < 20; i++) { System.out.println("wang"+i); } } }

如果像这样 我们用start来执行线程thread_first 那么可以实现与主线程交替执行的多线程效果:
wang0 miao0 wang1 miao1 wang2 wang3 miao2 wang4 miao3 wang5 miao4 wang6 miao5 wang7 miao6 wang8 miao7 wang9 miao8 wang10 miao9 wang11 miao10 wang12 miao11 wang13 miao12 wang14 miao13 wang15 miao14 wang16 miao15 wang17 miao16 miao17 miao18 wang18 miao19 wang19

如果使用run 那就是单纯执行而已 如下:
miao0 miao1 miao2 miao3 miao4 miao5 miao6 miao7 miao8 miao9 miao10 miao11 miao12 miao13 miao14 miao15 miao16 miao17 miao18 miao19 wang0 wang1 wang2 wang3 wang4 wang5 wang6 wang7 wang8 wang9 wang10 wang11 wang12 wang13 wang14 wang15 wang16 wang17 wang18 wang19

可见thread_first先执行了
为什么start可以多线程而run不行?
start的底层就包含了创建线程的过程,而run则只是把我们override里边的程序执行罢了
Thread实现图片下载 前面那个例子有点尬 也就是直观体验一下 现在来个稍微有点用的例子 下载图片。首先实现一下,下载功能的对象
package com.base.threadlearning.download_pic; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; public class Downloader { public void download(String url, String name){try { FileUtils.copyURLToFile(new URL(url),new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("download failed"); } }}

然后我们来写Thread 的demo 验证一下两个问题
1 先start的线程一定先执行嘛?如果不是 那是谁决定顺序呢?
2 效率真的有提升吗?(时间减少)
我们编写demo 如下
package com.base.threadlearning.download_pic; public class Thread_download_pic extends Thread{ private String url; private String name; public Thread_download_pic(String url, String name){ this.name = name; this.url = url; } @Override public void run(){ Downloader downloader = new Downloader(); downloader.download(url,name); System.out.println("download "+name+" done"); }public static void main(String[] args) { String _url = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604058853499&di=65a5ee6cea043252fd2385db8658e11f&imgtype=0&src=https://www.it610.com/article/http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201410%2F17%2F20141017162807_4JuXA.thumb.700_0.jpeg"; String _name = "D:\\Users\\Ryan\\IdeaProjects\\jvm_base_knowledge\\src\\main\\java\\com\\base\\threadlearning\\download_pic\\saber\\"; Thread_download_pic thread_download_pic_1 = new Thread_download_pic(_url,_name+"1.jpg"); Thread_download_pic thread_download_pic_2 = new Thread_download_pic(_url,_name+"2.jpg"); Thread_download_pic thread_download_pic_3 = new Thread_download_pic(_url,_name+"3.jpg"); thread_download_pic_1.start(); thread_download_pic_2.start(); thread_download_pic_3.start(); } }

下载的名称与路径_name 你们可以自定义 我这里也不隐藏我的绝对路径了 反正黑客破解了也找不到什么值钱的东西2333:)
这里注意 我下载器这个类对象Downloader是单独拉出来一个文件放置的
毕竟只要和main所在类Thread_download_pic 同一目录 可以直接调用 当然你把它放在里面当成内部类也可以 我是为了方便共同调用 抽离出来而已(我们后面还要用Runnnable Callable来实现这个demo)
目录结构如图:
Java|Java 从多线程到并发编程(一)——初识线程 进程 多线程 Thread Runnable 匿名内部类
文章图片

然后我们运行 可以明确 先start与后start确实不影响线程执行的顺序
Java|Java 从多线程到并发编程(一)——初识线程 进程 多线程 Thread Runnable 匿名内部类
文章图片

实际上是CPU轮询线程 CPU决定线程执行的先后 但是放心 谁先执行都一样 因为CPU频率太高 几乎可以看成同时执行,当然 这与真正的并行还相差甚远,尤其没有耗时操作的时候,这类单核CPU跑多线程和单线程效果几乎相同,但是我们这个应用因为有网络请求这种耗时操作 故具有优化的空间
如果是多核CPU那就真正实现了完全或者部分多线程 不过。。核数往往不太可能超过线程数
Thead的改进 匿名内部类 上面下载图片的案例中,我们其实巧妙地运用了一点设计模式,通过传入url,实现只需要一个类就完成三个Thread 图片下载机器人的实例化,而如果写死url,那么你将会需要三个类,有多少机器人你就得写多少类。而且有些情况,因为类的实现各有不同,没办法利用模板思维,而是只能写n个类的情况,如下:
class FirstRobot extends Thread{ @Override public void run(){ System.out.println("I am the first robot"); } }class SecondRobot extends Thread{ @Override public void run(){ System.out.println("I am the first robot"); } }class ThirdRobot extends Thread{ @Override public void run(){ System.out.println("I am the first robot"); } }

这时代码变得有点臃肿——类泛滥了
这时我们可以采用匿名内部类的方式,避免类的泛滥。采用这个方法的根本在于,这个类其实只使用一次,而且没有复用,代码如下
Thread robot1 = new Thread(){ @Override public void run() { super.run(); System.out.println("I am the first robot"); } }; Thread robot2 = new Thread(){ @Override public void run() { super.run(); System.out.println("I am the second robot"); } }; Thread robot3 = new Thread(){ @Override public void run() { super.run(); System.out.println("I am the third robot"); } }; robot1.start(); robot2.start(); robot3.start();

Thread = 线程? 请注意一个问题,很多兄弟会误认为,这个类如果继承了Thread对象,他就是线程本身了:)
其实继承Thread类的这个线程类Thread,只是我们jvm里边,对应那个线程的“外壳对象”罢了,我们通过这个对象来配置这个线程的执行内容(任务内容)以及设定其状态,但这个对象和线程绝不是一个东西
我这里补充一下整个底层的过程:
  • 首先 创建Thread对象
  • 对象实例执行start方法 注意这是个本地native方法 这时线程还没有真正被创建出来
  • 既然是native方法 我们知道java底层是C实现的 这里我们的start方法就是调用了JNI(Java Native Interface)
  • 那底层代码实现了啥?自然是向OS申请分配资源 创建线程,
  • 如果申请成功 则回调函数run得以执行 这样我们给此线程配置的任务就能执行
现在知道为啥要override run方法了吧?
总结 ′?` 其实很多细节存在问题 尤其是统计运行时间这个方面 我学习还不够深入 见谅
下一节我们将看到 单继承Thread的局限性,以及其多样的解决方案。
【Java|Java 从多线程到并发编程(一)——初识线程 进程 多线程 Thread Runnable 匿名内部类】下一节 Java 从多线程到并发编程(二)——Runnable Callable
  • 本文专栏
    • Java专栏
  • 我的其他专栏 希望能够帮到你 ( ?? ω ?? )?
    • MySQL专栏
    • 手把手带你学后端(服务端)
    • python应用
  • 谢谢大佬支持! 萌新有礼了:)

    推荐阅读