JAVA高级编程之多线程(看完这一篇就够了)

在这里插入图片描述



一、进程

这是我们能够最直观的从计算机中看到的进程的样子,你是否会发出这样的疑问:这东西有什么用呢?这东西是个什么玩意儿啊?
别着急,我们带着疑问去深度的了解它。
在这里插入图片描述

1、进程的由来(为什么会出现进程?)

1、提到进程,我们不得不从梦想开始的地方讲起,而梦想开始的地方在CPU上;

1.1、什么是CPU?

1、计算机中,按照冯诺依曼体系把计算机硬件分为了5部分,分别对应:存储器、运算器、控制器、输入设备、输出设备;而运算器和控制器就组成了CPU、中文名叫中央处理单元;

2、我们一个程序要想执行,第一步就得把它从磁盘或者其他外存上移到内存中去,这一步操作系统帮我们做了;

3、放到内存之后,控制器负责对程序指令进行解释执行,运算器负责对数据进行运算,而它们的执行过程是如下图的样子:
在这里插入图片描述

1.2、CPU是如何工作的?

我们只需要给CPU开始执行第一条指令的位置,也就是PC指针指向的第一个位置后,这条指令就会被控制器解释并执行,就像厨师开了菜谱的第一步之后开始做第一步,然后做完了之后,PC指针就会自动指向下一条指令,然后控制器再一次解释执行,以此类推直到厨师做完这道菜为止;那么整个过程中,控制器就好比厨师的大脑,运算器就好比厨师的双手,菜谱就好比加载到内存中的程序被编译后生成的一条条指令,而PC指针就是厨师的双眼,眼睛看菜谱,大脑调动双手去做菜,最后完成这么一个程序的执行任务。

那么我们既然要用CPU这个东西,我们也必然需要去对它进行正确的管理,不然就会出现问题,比如说:现在我们只给CPU一个PC指针指向的起始位置,然后让CPU自动完成工作,会出现什么问题?

在这里插入图片描述

我们下面来看一段程序:

在这里插入图片描述
上图程序右边第一个黑框表示的是有IO指令时这段程序的执行时间,第二个黑框表示的是把IO指令换成普通的计算指令后的执行时间,而他们的比值约为1000000:1。

CPU的工作是解释执行程序,然后对数据进行运算,而程序最终结果的打印,也即是IO指令是由外部设备(输入/输出设备)来完成的,也就是说CPU解释执行一条IO语句,那么操作系统就会把控制权转交给外设,而转交需要时间,外设工作需要时间,并且外设的工作时间远比CPU长,CPU是通过电路进行工作的,外设是机器,所以它两的工作效率有天壤之别;

我们想一想,当IO设备在工作的时候,CPU是什么状态?

此时的CPU会等待IO设备执行完成,在此期间,CPU处于空闲状态,由此我们是不是可以发现一个特别重要的问题:那就是CPU的利用率不高。
我们发明计算机的目的就是需要其做大量的复杂的科学计算,而现在它几乎一半的时间是在等待,是在处于空闲状态,这是我们科学家不想要看到的结果。

既然发现了问题,怎么样去解决呢?

1.3、如何解决CPU利用率不高的问题?

这个时候我们就不得不举一个生活中的例子了,比如说:
你现在正开始做饭,然后你首先第一步就是把饭煮上了,在饭煮熟之前的这段时间,你是选择等饭熟了之后再开始炒菜,还是在煮饭的同时,你就开始炒菜?我相信聪明人都应该选择后者,如果你不选,这不能直接说明你不太聪明,但至少可以说明你根本不饿。

那么我们的CPU就是那个聪明的小朋友:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
那么我们怎么去具体实现在一个CPU上交替执行多个程序,以此来提高CPU的利用率呢?方案现在给你提出来了,你怎么去执行它?

在这里插入图片描述
我们如果单单只是修改PC指针的指向就行了吗?当然是远远不够的,为什么?

程序1的ax它本应该等于2,但由于执行到52行时,PC切出去了,切到了200行,当执行完程序2后,PC指针再切到52行,此时的ax就变成了20,程序1受到了程序2的影响,结果出现了偏差,计算失败告终!!!

所以现在我们需要有一个东西去把程序运行时的每一刻该是什么样子的,给记录下来,就像是看电视剧时按下了暂停键,保护好案发现场,而这个东西就被科学家给创造出来了,它就是PCB,全称process control block,进程控制块。

哦!!!!到这里终于出现了进程两个字了,什么是进程,相信大家都差不多可以猜到了:

我们不难发现,静态的程序和在内存上运行起来的程序是不是完全不同的样子,那么我们该怎么去描述一个程序动态执行的过程?没错,我们给它取了个响当当的名字,那就是进程!!!

2、进程的概念

我们来总结一下什么是进程:
1、一个具有独立功能的程序在一个数据集合上的一次动态执行过程;
2、在操作系统中,每个独立运行的程序就是一个进程,当一个程序进入内存运行时,即变成了一个进程;
3、进程是操作系统进行资源分配和调度的一个独立单位,是具有独立功能切处于运行过程中的程序。

2.1、进程三大特性

1、独立性:每个进程都拥有自己的私有地址空间,其他进程不可以直接访问该地址空间,除非在进程本身允许的情况下;

2、动态性:程序只是一个静态的指令集合,进程是一个正在内存中运行的、动态的指令集合,进程具有自己的生命周期和各种不同状态;

3、并发性:多个进程可以在单个处理机(CPU)上并发执行,多个进程之间互不影响;并发又可以解释位同时出发,交替执行。


二、线程

1、线程的由来(为什么又会出现线程?)

我们来看一个例子:
在这里插入图片描述
怎么去开发这个软件?或者说以什么方式去开发这个软件?

方案一:在这里插入图片描述
这个方案播放出来的声音肯定是不连贯的,为什么?
CPU会出现长时间空闲状态。

方案二:
在这里插入图片描述
这个方案看起来是可行的,分别为三个核心功能模块单独创建一个进程,这样确实可以解决单进程空闲的问题,但是这么做合理吗?有没有不好的地方?

有:
1、进程之间如何通信和共享数据?
我们知道进程它是具有独立性的,要想它们之前互相访问,必须得双方允许才行,可是这样好像有点浪费时间精力吧,你去拿个东西或者办件事,每次都得需要别人同意,每次都需要你把证明准备好,这是不是有点分散精力了,有点本末倒置;

2、维护进程的系统开销较大
创建进程时,需要分配资源,建立PCB,进程越多,内存占用率就越高,一件事却需要消耗那么多内存,想来确实不划算;撤销进程时,又需要回收资源,撤销PCB,但回收的资源有大有小,很难肯定适不适合下一个将要创建的进程;进程切换时又需要保存状态信息,所以在资源开销上确实是不太合理,有点杀鸡焉用牛刀的感觉。

有没有一种既可以节省开销又可以像进程一样切来切去的方法来解决这些矛盾呢?
有,这就是线程的由来。
在这里插入图片描述

2、线程的概念

我们下面就来介绍线程(thread)的概念:
1、线程是进程的组成部分,一个线程必须在一个进程之内,而一个进程可以拥有多个线程;
2、线程是最小的处理单位,线程可以拥有自己的堆栈、计数器和局部变量,但不能拥有系统资源,多个线程共享其所在进程的系统资源;
3、线程可以完成一定的任务,使用多线程可以在一个出现中同时完成多个任务,在更低层次中引入多处理任务。
在这里插入图片描述

2.1、线程与进程的区别

1、多进程之间的数据块是相互独立的,彼此互不影响,进程之间需要通过信号、管道等进行交互;
2、多线程之间的数据块可以共享,一个进程的各个线程可以共享程序段、数据段等资源。
3、线程是最小的处理单元,而进程是操作系统进行资源分配和调度的独立单位。

2.2、线程相比于进程的优势

1、多线程之间可以共享内存,节省系统资源成本;
2、多线程之间进行切换更加快速,节约了时间成本。


3、Thread类核心操作

在这里插入图片描述
在这里插入图片描述

3.1、线程创建

(1)通过继承Thread的方式创建线程

步骤:
1、定义一个子类继承Thread类,并重写run()方法;
2、创建子类对象,即实例化线程对象;
3、调用线程对象的start()方法启动线程。
在这里插入图片描述

(2)通过实现Runnable的方式创建线程

步骤:
1、定义一个类实现Runnable接口,并实现该接口中的run()方法;
2、创建一个Thread类的实例,并将Runnable接口实现类创建的对象作为参数传入Thread类的构造方法中;
3、调用Thread对象的start()方法启动线程。
在这里插入图片描述

(3)通过实现Callable的方式创建线程

步骤:
1、创建Callable接口的实现类,并实现call()方法,该方法将作为线程的执行体,并且有返回值,然后创建Callable类的实例对象;
2、使用FutureTask类来包装Callable对象,在FutureTask对象中封装了FutureTask对象的call()方法的返回值;
3、使用FutureTask对象作为Thread对象的target,创建并自动启动线程;
4、调用FutureTask对象的get()方法来获取子线程执行结束后的返回值。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Task implements Callable<Integer>{
    public Integer call() throws Exception{
        int i = 0;
        for (;i<50;i++){
            System.out.println("Callable实现了的子线程");
        }
        return i;
    }

}
public class CallableDemo {
    public static void main(String[] args) {
        FutureTask<Integer> task = new FutureTask<Integer>(new Task());
        Thread th = new Thread(task,"子线程");
        th.start();
        try {
            System.out.println(task.get());
        }catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i=0;i<=50;i++){
            System.out.println("我是主线程");
        }
    }



}

在这里插入图片描述

(4)通过基于lambda的方式创建线程

在这里插入图片描述


4、线程属性

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.1、如何中断一个线程

在这里插入图片描述

4.2、如何等待线程结束

在这里插入图片描述

4.3、如何获取线程的引用

在这里插入图片描述

4.4、如何进行线程休眠

在这里插入图片描述


5、线程状态及线程状态的相互转换

在这里插入图片描述

新建(New):此时其与java对象一样,仅由JVM为其分配内存并初始化。新建状态的线程没有任何动态特征,程序也不会执行线程的执行体;

就绪:当线程对象调用start()方法后,线程进入就绪状态,相当于“等待执行”,此时,调度程序就可以把CPU分配给该线程,JVM会为线程创建方法调用栈和程序计数器,此时的线程只是准备就绪等待执行;
注意:new 完一个线程只能调用一次start方法;

运行:处于就绪状态的线程获取到CPU后,开始执行run()方法,如果计算机的CPU是单核的,则在任意时刻只有一个线程处于运行态,一个线程不可能一直处于运行状态,除非线程的执行体足够短,瞬间就执行完毕;

阻塞:线程在运行过程中需要中断,目的是使其他线程获取执行的机会,线程调度的细节取决于底层平台采取的策略,正在执行的程序被阻塞后,其他线程就可以获取执行的机会,被阻塞的线程会在合适的时候重新进入就绪状态;

死亡:线程结束后,就会处于死亡状态,结束进程有三种方式:
1、线程执行完run()、call()方法后,线程正常结束;
2、线程抛出一个未捕获的异常或错误;
3、调用stop()方法强行结束线程。

还有的资料是7种状态

在这里插入图片描述

6、线程安全

6.1、什么是线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的
我们先来看一个例子,比如说:现在电影院售票100张,分三个窗口卖,让你设计一个程序完成这个系统?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们期望的结果是,三个窗口同时售卖总数为100张的电影票,且不允许重复,这才是我们想要的结果,可是现在3个窗口开始卖同一张票了,这就是线程不安全问题,此售票系统线程不安全。
所以我们再次看线程安全的概念:当多个线程访问同一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不用同步处理,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

总结一下线程安全的条件:
1、多线程访问同一数据块或者对象;
2、线程之间存在交互或运行时有调度;
3、不需要额外的同步;
4、调用方进行任何操作之后,再访问这个对象或者数据块;
5、满足以上4点之后还能得到正确的结果。

我们才能称这个对象为线程安全的。

6.2、线程不安全的原因有哪些?

在这里插入图片描述
如果说是一个相对独立的线程,它不涉及到共享内存的部分,这样的线程一般是安全的;所以一个线程安全与否,可以通过两点判断,是否是确定的,是否是可重现的

其实我们可以反推出来:
1、多个线程共享内存数据;
2、多个线程再运行时存在调度或者是彼此之间存在交替运行;
3、即使如此也没有进行额外的同步处理;
4、调用方人为的进行一些违规调度操作。

6.3、如何确保线程安全

线程同步

使用线程同步可以避免线程不安全问题,java使用监控器(对象锁)实现同步,每个对象都有一个对象锁,使用对象锁可以保证一次只允许一个线程执行对象的同步语句,即在对象的同步语句执行完毕之前,其他试图执行当前对象的同步语句的线程都将处于阻塞状态,当线程在当前对象的同步语句执行完毕后,对象锁才会打开,并让优先级高的阻塞线程处理同步语句。

1、同步代码块:
在这里插入图片描述
看个生动的例子:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
如果我们用同步代码块,能不能解决售票系统问题?
在这里插入图片描述
答案是可以

注意:1、 synchronized (Ticket.class)不可以放到循环前面,即外面;
2、 synchronized (锁对象)中锁对象可以用Ticket.class代替,因为它也是唯一的

2、同步方法
在这里插入图片描述

技巧:先写出同步代码块,再把它改成同步方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3、同步锁
在这里插入图片描述
在这里插入图片描述


死锁问题

在这里插入图片描述
举一个生活中的例子:比如马路上的单行道,当两辆车在单行道上相遇时(相对而行的时候),那么这个时候就会发生堵车,谁也过不去,那么这就是一个死锁;如果还没懂,我们再来看一个例子:
在这里插入图片描述
A、B手上都握有彼此所要的资源,但双方都不知道,然后一直等,等到啥时候有了再运行,那么它们就会一直等下去,这就是一个死锁。

在这里插入图片描述

遇到死锁该怎么办

下面我们就来看看如何处理死锁

7、线程等待/通知

7.1、等待唤醒机制

在这里插入图片描述
在这里插入图片描述

wait 内部的工作过程是怎样的

让当前线程等待,并释放对象锁,直到其他线程调用该监视器的notify()或notifyAll()来唤醒该线程。

为什么 wait 和 notify 要在 synchronized 内部使用

一个有难度的 Java 问题,wait 和 notify。 它们是在有 synchronized 标记的方法或 synchronized 块中调用的,因为 wait 和 nodify 需要监视对其调用的 Object。


总结

好了本章就讲到这里,对于多线程的知识还有很多是本文没有讲到了,小编也会在后面再更新一期多线程的讲解,如果小伙伴们觉得讲到不错,就给小编点个关注吧!!!!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无树菩提~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值