多线程基础
发布日期:2021-07-22 10:54:21 浏览次数:2 分类:技术文章

本文共 8228 字,大约阅读时间需要 27 分钟。

多线程

线程使用的目的:

在程序中完成某一功能的时候吗,我们会将他描述成任务,这个任务需要在线程中完成

串行与并发:

如果在程序中,有多个任务需要被处理,此时的处理方式可以有串行和并发:

串行(同步):所有的任务,按照一定的顺序,依次执行,如果前面的任务没有执行结束,后面的任务等待

并发(异步):将多个任务同时执行,在一个时间段内,同时处理多个任务

生活中,其实有很多串行和并发的案例,最常见的就是排队买饭,小明到KFC吃饭,发现有好几个窗口可以点餐,选择了其中一个窗口进行排队,此时,KFC采用的模式就是串行加并发的模式。每个窗口之前,有很多顾客在排队,此时他们的任务就是串行的,前面的顾客没有处理完之后,后面的顾客只能等待,同时多个窗口之间的顾客是可以同时点餐的,他们是并发的.使用并发任务,在一定程度上提高了效率。例如:小明下班回到家,需要洗衣服,做饭,扫地,假设,三件事情各自耗时10分钟,那么这些任务如果都给小明一个人做,那小明就需要30分钟,但是如果小明找两个帮手,他们三个人每人干一件事,则总共耗时10分钟在程序中,有些任务是比较耗时的,特别是涉及到非常大的文件的处理,或者网络文件的处理。此时就需要使用异步任务来处理,否则就会阻塞主线程,导致用户的交互卡顿,合适的使用并发任务,可以在一定程度上提高程序的执行效率

并发的原理:

一个程序如果需要被执行,必须的资源是CPU和内存,在内存上开辟空间,为程序中的变量进行数据的存储;同时需要CPU处理程序中的逻辑,现在处于一个硬件过剩的时代,但是即使是在硬件不发达的时代,并发任务也是可以实现的,以单核CPU为例,处理任务的核心就只有一个,那就意味着,如果CPU在处理一个程序中的任务,其他的程序都得暂停,那么并发是如何实现的?

其实所谓的并发,并不是真正意义上的多个任务同时执行,而是CPU快速的在不同任务之间进行切换,在某一个时间点处理任务A,下一个时间点去处理任务B,每一个任务都没有立即处理结束,CPU快速的在不同的任务之间进行切换,只是这个切换的速度非常快,人类是识别不了的,因此会给人一种”多个任务同时执行“的假象

因此,所谓的并发,其实就是CPU快速的在不同的任务之间切换的一种假象

思考:既然多个任务并发,可以在一定程度上提高程序的执行效率,那么并发数量是不是越高越好?并不是,多个任务的并发,其实就是CPU在不同的任务之间进行切换,如果并发的数量过多,会导致分配到每一个任务上的CPU时间片较短,也并不见得会提高程序的执行效率,而且每一个任务的载体(线程)也是需要消耗资源的,过多的线程,会导致其他资源的浪费。例如:上述案例中,我们说到了小明雇保姆干活,那么是不是保姆越多越好呢?显然不是,雇保姆就得花钱,类似于开辟线程执行并发的任务就需要消耗资源一样,那么在雇保姆的时候就得想,我真的需要这么多保姆吗?家里有十件事情需要处理,那么就一定需要雇佣十个保姆吗?结果显然不需要

进程和线程:

进程:是对一个程序在运行过程中,占用的各种资源的描述

线程:是进程中的一个最小的执行单元,其实吗,在操作系统中最小的任务执行单元并不是线程,而是句柄,只不过是句柄太小了,操作起来非常麻烦,因此线程就是我们可控的最小的任务执行单元

其实对于操作系统来说,一个任务就是一个进程,例如打开了QQ,就是一个QQ的进程;再打开一个QQ,就是一个新的QQ的进程,打开了一个微信就是一个微信的进程,在一个任务中,有的时候需要同时处理多件事情,例如打开了一个QQ音乐,需要同时播放声音和播放歌词,那么这些进程中的子任务,就是一个个的进程

每一个进程至少需要处理一件任务。因此,每一个进程中至少要包含一个线程,如果一个线程都结束了,那么这个进程也就结束了

多个线程的同时执行,是需要这些线程去争抢CPU资源,而CPU资源的分配是以时间片为单位的,即某一个线程抢到了0.01秒的CPU时间片,在这个时间内,CPU处理这个线程的任务,至于哪一个线程能抢到CPU时间片,则有操作系统进行资源调度

进程和线程的异同:

相同点:进程和线程都是为了处理多个并发任务而存在的

不同点:进程之间是资源不共享的,一个线程中不能访问另外一个进程中的数据,而线程之间是资源共享的,多个线程可以共享同一个数据,也正因为线程之间是资源共享的,所以会出现临界资源的问题

进程和线程的关系:

一个进程在开辟的时候,会自动创建一个线程,来处理这个进程中的任务,这个线程被称为是主线程,在程序运行的过程中,还可以开辟出来其他线程,都是子线程

也就是说,一个进程中,是可以包含多个线程,一个进程中的某一个线程崩溃了,只要还有其他线程存在,就不会影响整个进程的执行,但是如果一个进程中,所有的线程都执行结束了,那么这个进程也就终止了

总结:

程序:一个可以执行的文件

进程:一个正在运行的程序,也可以理解成在内存中开辟了一块空间

线程:负责程序的运行,可以看作一条执行的通道或是执行单元,所以我们通常将工作原理理解成线程的工作

进程中必须有线程,至少有一个

当有一个线程的时候,我们称之为单线程(唯一的线程就是主线程)

当有一个或多个线程同时存在时,我们称为多线程

多线程的作用:为了实现同一时间干多件事情(并发执行)

线程的生命周期:

线程的状态:

线程的生命周期,指的是一个线程对象,从最开始的创建到最后的销毁,中间所经历的过程。在这个过程中,线程对象处于不同的状态

New:新生态,一个线程对象刚被实例化完成的时候,就处于这个状态

Runnable:就绪态,处于这个状态的线程,可以参与CPU时间片的争抢

Run:运行态,某一个线程抢到了CPU时间片,可以执行这个线程中的逻辑

Block:阻塞态 ,线程由于各种原因,暂时挂起,处于阻塞(暂停)态,这个状态的线程,不参与CPU时间片的争抢

Dead:死亡态,线程即将被销毁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gerwNeFl-1622967435401)(C:\Users\16326\Desktop\dayHomework\数据结构\QQ截图20210601224804.png)]

理解多线程:

对线程并发执行的说明:

简单理解(cpu单核):从宏观上看,线程有并发执行,从微观上看,并没有,在线程完成任务时,实际工作的是cpu,我们将cpu工作描述为时间片(单次获取cpu的时间,一般在几十毫秒),cpu只有一个,本质上同一时间只能做一件事情,因为cpu单词=次时间片很短,短到肉眼无法区分,所以当cpu在多个线程之间快速切换时,宏观上给我们的感受是多件事情同时在执行

注意:

1.cpu是随机的,线程之间本质上默认是抢cpu的状态,谁抢到了cpu的状态,谁就获得了时间片,就工作,所以多个线程的工作是随机默认的

2.在使用多线程时,并不是线程数越多越好,本质上大家使用同一个cpu,完成任务的时间并没有减少,要跟具体的情况创建线程,多线程是为了实现同一时间完成多件事情的目的,比如我们用手机打开一个app时,需要滑动界面浏览,同时界面的图片需要下载,对于这两个功能最好同时进行,这时可以使用多线程

多线程的实例演示:

/*代码演示的是主线程和垃圾回收线程在同时工作的状态线程工作的地方称为任务区每一个线程都有一个任务区,任务区通过对应的方法产生作用JVM至少有两个线程:主线程:任务区:main函数垃圾回收线程:任务区:finalize函数*/public class Demo{
//就是主线程的任务区 public static void main(String[] args){
new Test(); /* 手动运行垃圾回收器 */ System.gc(); System.out.println("main");}}class Test{
@Override /* 重写finalize方法 */ protected void finalize throws Throwable{
System.out.println("finalize"); }}/*代码说明:1.gc()方法:垃圾回收器原理:当执行gc方法时,会触发垃圾回收机制,开启垃圾回收线程,执行finalize方法2.finalize()方法:垃圾回收线程的任务区正常情况下,这个函数是由系统调用的,重写只是为了更好的观察多线程的发生3.线程和任务的关系任务区结束,线程随着任务的结束而结束,线程随着任务的开始而开始,当线程还在工作的时候,进程不能结束对于主线程而言,当main方法结束之后,主任务区结束,主线程结束对于垃圾回收线程来说,当finalize函数结束,垃圾回收任务结束,垃圾回收线程结束4.通过运行程序,我们发现字符串main和字符串finalize的打印顺序是随机,说明CPU的特性是多个线程之间抢CPU的关系,说明CPU有随机性

创建线程:

默认情况下,主线程和垃圾回收线程都是由系统创建的,但是我们需要完成自己的功能,所以需要创建自己的线程

java将线程面向对象了,形成的类就是Thread,在Thread内部执行任务的方法叫run()

线程的实例化:

public class Demo2{
//为了方便研究,暂时不考虑垃圾回收线程 public static void mian(String[] args){
//创建自己的线程---还没有线程的功能 Thread t1 = new Thread(); Thread t2 = new Thread(); //当执行start方法之后,他才有了线程的功能--开启线程 t1.start(); t2.start(); //此时就有了三个线程,主线程加两个子线程 }}
创建线程的两种方式:
继承Thread类
1.线程对象刚刚被实例化的时候,线程处于新生态,还没有线程的功能,如果要让这个线程执行他的任务,需要调用start()方法,使线程进入到就绪态,争抢时间片2.开启线程的方法是start()而不是run(),是因为线程获取cpu是随机的,run是线程的任务区,代表功能,如果手动执行run,此时的线程可能还没有拿到cpu,无法工作,操作失败,通过start方法,让线程处于就绪状态,随时拥有抢cpu的能力,当抢到cpu后,再自动执行run,实现并发的任务3.我们之所以使用Thread类的子类对象,是因为我们实现的实际功能,Thread类作为系统类不能提前知晓,所以无法将功能代码放入Thread的run方法里,如果想实现自己的功能,可以写Thread类的子类,重写run()方法,这也就是为什么Thread类中run方法是一个空方法
1.继承自Thread类,做一个Thread的子类,在子类中,重写父类的run方法,在这个重写的方法中,指定这个线程需要处理的任务2.Thread.currentThread():可以用在任意位置,获取当前的线程。如果是Thread的子类,可以在子类中,使用this获取到当前的线程3.当我们手动调用run的时候,他失去了任务区的功能,变成了一个普通的方法,当run作为一个普通的方法时,内部对应的线程跟调用他的位置保持一致4.结果分析:主线程和两个子线程之间是随机打印的,他们是抢cpu的关系5.通过创建Thread子类的方式实现功能,线程与任务绑定在了一起,操作不方便解决方式:我们可以将任务从线程中分离出来,哪个线程需要工作,就将任务交给谁,操作方便,灵活使用Runnable接口
public class Demo{
public static void main(String[] args){
//还是为了简单,我们暂时不考虑垃圾回收线程 MyThread t1 = new MyThread("chen"); Mythread t2 = new MyThread("heng"); t1.start(); t2.start(); //手动调用run()方法 t1.run(); for(int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName() + " i:" + i); } }}class MyThread extends Thread{
String name; public MyThread(String name){
this.name = name; } /* 任务区 重写run方法,实现我们的功能,run就是我们的任务区 Thread.currentThread():获取的当前的线程 Thread.currentThread().getName():线程的名字 */ @Override public void run(){
for(int i = 0;i < 10;i++){
System.out.println(this.name+" "+Thread.currentThread().getName()+" i:"+i); } } }
实现Runnable接口

在Thread类的构造方法中,有一个重载的构造方法,参数是Runnable接口,因此可以通过Runnable接口的实现类对象进行Thread对象的实例化

如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回

优缺点:

继承的方式:优点在于可读性比较强,缺点在于不够灵活,如果要定制一个线程,就必须要定制一个线程,就必须要继承自Thread类,可能会影响原有的继承体系。

接口的方式:优点在于灵活,并且不会影响一个类的继承体系,缺点在于可读性较差

线程名字的设置:

每一个线程都有一个名字,如果实例化线程的时候,不去设定名字,那么这个线程会拥有一个默认的名字

线程的礼让:

线程礼让,就是当前已经抢到CPU资源的正在运行的线程,释放自己持有的CPU资源,回到就绪状态,重新参与CPU时间片的争抢

线程同步:

临界资源问题:

在一个线程中,多个线程之间的资源可以共享,如果在一个进程中的一个资源同时被多个线程访问,这个资源就是一个临界资源

如果多个线程同时访问临界资源,会对这个资源造成影响

临界资源问题:

多个线程同时访问一个资源的情况下,一个线程在操作这个资源的时候,将值取出去运算,还未来得及修改这块空间的值之前,值又被其他线程取走了,此时就会出现临界资源得问题,造成这个资源的值出现不是我们预期的值

解决方案:

临界资源问题的出现的原因就是多个线程同时访问一个资源,因此解决方案也很简单,就是不让多个线程同时访问即可

在一个线程操作一个资源的时候,对这个资源进行上锁被锁住的资源,其他线程无法再进行访问

线程锁:

线程锁,就是用来锁住一个临界资源,其他线程无法进行访问,在程序中可以分为对象锁,类锁

对作为锁的对象的要求:

必须是对象,必须保证被多个线程共享

对象锁:任何普通对象或者this,都可以被当做是一把锁来使用,但是需要注意,必须要保证不同的线程看到的锁,需要是同一把锁才能生效,如果不同的线程看到的锁对象是不一样的,此时这把锁将没意义

注意:不能直接使用匿名对象作为锁,因为每次这样都是在重新new,要保证锁是被大家共享的

类锁:可以将一个类做成锁,使用类.class(类的字节码文件对象)来作为锁,因为类的字节码文件对象使用范围太大,所以我们一般不使用他作为锁,只有在静态方法中。

synchronized:

如果在一个方法中,所有的逻辑都需要放在一个同步代码块中执行,那这样的方法,可以直接做成同步方法。

同步方法:

1.静态同步方法,使用的对象锁(this)

是某个对象实例内,synchronized Method(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象中有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其他的线程不能同时访问这个对象中任何一个synchronized方法),这时,不同的对象实例的synchronized方法是互不干扰的,也即是说,其他线程可以同时访问相同类得另一个实例对象的synchronized方法

2.静态同步方法,使用的类锁(当前类的.class文件)

是某个类的范围,synchronized static method()可以防止多个线程同时访问这个类中的synchronized static方法,它对类的所有对象实例起作用

静态同步函数在进内存时不会创建对象,但是存在其所属类的字节码文件对象,属于class类型的对象,所以静态同步函数的锁是其所属的字节码文件对象

同步代码块:

synchronized关键字用于方法中的某个块中,表示只对这个区块的资源实行互斥访问

同步代码块是解决临界资源最常见的方式,将一段代码放到同步代码块中,将这段代码上锁

第一个线程抢到锁标记后,可以对这个紧接资源进行上锁,操作这个临界资源,此时的其他线程在执行到synchronized的时候,会进入到锁池,知道持有锁的线程使用结束之后,对这个资源进行解锁,此时,处于锁池中的线程都可以抢到这个锁标记,那一个线程抢到了,就进入到就绪态,没有抢到锁的线程依然位于锁池中

同步代码块的特点:可以保证线程的安全,由于每次都要进行判断处理,所以降低了执行效率

synchronized(锁(对象)){
同步的代码}

同步代码块和同步函数的比较

同步代码块使用更加灵活,只给需要同步的的部分代码块同步即可,而同步函数是给这个函数内的所有代码同步,

由于同步的代码越少越好,所以最好使用同步代码块

使用同步代码块和同步方法的时机:多个线程共享一个数据,至少有两个线程

synchronized在继承中的使用

synchronized关键字是不能被继承的,也就是说,基类的方法synchronized f() {}在继承中并不自动是synchronized f(){},而变成了f(){}。继承类需要你显示的指定它的某个方法为synchronized方法

单例模式:

懒汉式单例,在多线程的环境下,会出现问题,由于临界资源问题的存在,单例对象可能会被实例化多次,因此,单例设计模式,尤其是懒汉式单例,需要针对多线程的环境进行处理

所有代码同步,

由于同步的代码越少越好,所以最好使用同步代码块

使用同步代码块和同步方法的时机:多个线程共享一个数据,至少有两个线程

synchronized在继承中的使用

synchronized关键字是不能被继承的,也就是说,基类的方法synchronized f() {}在继承中并不自动是synchronized f(){},而变成了f(){}。继承类需要你显示的指定它的某个方法为synchronized方法

单例模式:

懒汉式单例,在多线程的环境下,会出现问题,由于临界资源问题的存在,单例对象可能会被实例化多次,因此,单例设计模式,尤其是懒汉式单例,需要针对多线程的环境进行处理

转载地址:https://blog.csdn.net/m0_56433040/article/details/117630098 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:IO流基础
下一篇:集合精通(Set + Map)

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2024年05月06日 14时24分53秒