关于多线程

并发编程

在计算机的操作系统中,我们了解到了进程管理,有了解到了cpu的特性,核心数和频率,在次之前我们所写的代码都是只用到了一个核心,此时无论你怎么优化代码,最多也只能使用到一个cpu的核心,把这个核心跑满了,其他的核心也是闲着,所以我们可以通过特殊的编写代码,把多个CPU的核心都能利用起来,这样的代码就称"并发编程",多进程编程也是一种典型的并发编程.

线程的由来

虽然多进程能够解决问题,但是随着对于效率要求越来越高,希望有更好的方式来进行并发编程,多进程编程,最大的问题就是太重了,创建进程/销毁进程,开销比较大(时间和空间),一旦需求场景需要频繁的创建销毁线程,开销就会非常明显了,最典型的就是服务器开发,针对每个发送请求的客户端,都创建一个独立的进程,由着个进程给客户端提供服务.为了解决进程开销的问题,发明了线程(Thread)

线程

线程可以理解成更轻量级的进程,也可以解决并发编程的问题,但是创建/销毁线程比进程开销更低,因此多线程编程成为更主流的并发编程方式,

所谓的进程,在系统中,是通过PCB结构体来描述的,也是通过链表的形式连接起来的,对于系统中,线程,同样也是通过PCB来描述的(Linux)

一个进程是一组PCB,一个线程是一个PCB,存在了包含关系,一个进程中至少有一个线程,也可以有多个线程,但不能没有线程,此时,每个线程都可以,独立到CPU上调度执行,线程是系统调度执行的基本单位 ,进程是系统进行资源分配的基本单位

一个可执行程序,运行的时候(双击),操作系统就会创建线程,给这个程序分配各种系统资源(CPU,内存,硬盘,网络带宽),同时也会在这个进程中,创建出这个一个或者多个线程,这些线程再去CPU上进行调度,

如果有多个线程在一个进程中,每个线程都会有自己的状态,优先级,记账信息,上下文,每个都会各自独立在CPU上调度执行,同一个进程中的这些线程,共用同一份系统资源,线程比进程更轻量,主要在于,创建线程省去了"分配资源"的过程,销毁线程也省去了"释放资源"的过程,一旦创建进程就会创建线程,系统就会分配资源,后续创建第二个第三个线程的时候,就不会再重新分配资源了.

适当提升线程数目,可以提高效率,关键是利用多核心,进行"并行执行",如果线程数目太多,超出了CPU的核心数,就会适得其反,线程之间相互抢占资源,由于多个线程,使用的是同一份资源(内存资源)->代码中定义的对象/变量,如果多个线程对同一个变量进行读写(尤其是写),就容易引起冲突,一旦发生冲突就可能程序出问题.

当一个进程中,有多个线程的时候,一旦某个线程抛出异常,这个时候,如果能够妥善处理,还好一旦处理不当,就可能导致进程崩溃,因此其他线程也会随之崩溃了.

所谓的"核心"是CPU的核心~硬件设备,而进程是系统管理的软件资源,进程也叫任务,任务需要交给核心来执行,每个进程中又可以包含多个线程

理解并行和并发 

在微观角度看,多个核心,每个核心都可以执行一个线程,这些核心之间的执行过程是"同时执行的"并行,

一个核心,"分时复用",来切换多个线程,多个线程是一个接一个的执行,由于调度速度够快,宏观是看起来好像是同时执行,称为并发执行

进程线程的概念区别

1.进程包含线程,一个进程中至少有一个线程,也可以有多个线程,但不能没有线程

2.进程是系统分配资源的基本单位,线程是系统执行调度的基本单位

3.同一个进程里的线程之间,共用同一份系统资源(内存,硬盘,网络带宽等.....)尤其是"内存资源",就是代码中定义的对象/变量,编程中,多个线程可以共用同一份变量.

4.线程是当下实现并发编程的主流方式,通过多线程,就可以充分利用好多核心CPU,但是也不是线程数目越多,越好线程数目达到一定程度,把多个核心都充分利用好了之后,此时继续增加线程,无法在提高效率,甚至可能会影响效率(线程调度,也是有开销的)

5.多个线程之间,可能会相互影响,线程安全问题,一个线程抛出异常,也可能把其他线程一起带走

6.多个进程之间,一般不会相互影响,一个进程崩溃了,不会影响到其他进程(这一点也成为"进程的隔离性")

如何在Java代码中,编写多线程程序

线程,本身是操作系统提供的概念,操作系统提供API,供程序员使用,不同的操作系统,提供的API是不同的,而Java(JVM)把这些系统API封装好了,只需了解Java提供的这一套API就行Thread(标准库),这个类就负责完成多线程的相关开发,此处这个类可以直接使用,不需要导入任何的包,在Java中,Java.lang默认自动导入

根据在实战中的需求来进行构造

        Thread类的构造方法
1.通过子类继承父类实现

重新run方法

随后通过调用start()方法,就会在进程内部创建出一个新的线程,新的线程就会执行刚刚run的代码,

此时没有调用run方法,但是最终被执行了,这样的方法就成为回调函数,用户手动定义了,但是没有手动调用,最终这个方法被系统/库/框架调用了

这个方法是Thread的静态方法,通过类名.方法名进行调用

2.通过实现Runnable接口来完成,实例一个Thread类,作为参数传入

重写run方法,这个方法解耦合\

3.匿名内部类的方式来编写

匿名内部类,类的定义,可以写属性,也可以写方法,

这个类的作用1.重写run方法,2.创建子类,3.创建子类的实例赋值给thread引用.

4.最重要的一个写法基于lambda表达式,来创建线程

lambda本质上就是简化的匿名内部类,我们在写匿名内部类的时候,本质上获取的是类里面的方法,而不是为了写类,所以我们可以使用lambda直接重写方法,以便于我们使用

 Thread类的方法和属性
获取当前线程的引用

public static Thread currentThread()

休眠线程

sleep(等待时间),使用sleep控制的是"线程休眠的时间"而不是两个代码执行的间隔时间

事实证明开始和结束的时间是>=1000的,此处设置的sleep时间,是线程阻塞的时间,1000ms之内,线程不回去cpu上执行的(阻塞),当时间到了之后,线程阻塞状态恢复到就绪状态,不代表线程就能立即去 cpu上执行,从恢复到去cpu上执行,还需要时间

ID,此处的是Thread对象的身份标识,JVM自动分配不能手动设置,,通常情况下一个Thread对象,就是对应系统内部的一个线程(PCB),但是也可能存在一种情况,Thread对象存在,但是系统内部的线程没了或还早创建

从编译角度理解:静态理解成编译过程中确定的,动态理解成运行过程中确定的,除了重写的方法,动态绑定,运行时确定,不去重写,也就是静态的

关于前台线程和后台线程

后台线程:如果这个线程在执行过程中,不能阻止进程结束,(虽然线程在执行过程中,但是进程要结束了,此时线程也会随之被带走),这样的线程就叫做后台线程.也叫守护线程

前台线程:如果线程在执行过程中,能够阻止进程的结束,此时就是前台线程

创建的新线程默认都是前台线程,在Thread中有一个方法isDaemon(),可以设置是否为后台线程,

这时main方法中没有其他的代码,只有两个线程,都是前台线程,main线程执行完毕,但是Thread这个线程还在执行,所以进程还在继续,

此时加上setDaemon(true)这条语句,thread就被设置成后台线程,只有main一个前台线程,后台线程当前台线程执行完毕时,也会结束.

方法isAlive()

创建的new Thread对象,生命周期和内核中实际的线程,是不一定一样的,可能会出现,Thread对象仍然存在,但是内核中的线程不存在的情况(1.没有执行start()方法内核中还没有创建线程,2.run()方法执行完毕)但是Thread对象还在

(但是不会出现线程存在,Thread对象不存在的情况)

例如上述代码这个代码实例化Thread的对象,在start之前打印了一下thread的生命状态,

由于线程之间的调度顺序是不一样的,如果两个线程sleep的时间都是3000,当3000到的时候,两个线程谁先执行谁后执行不一定,此处的不一定不是指双方概率均等,实际说有很大的差异

关于start和run的之间的差别

1.run是描述线程的任务,类似与线程的入口

2.start,调用系统函数,真正在系统内核中,创建PCB(将PCB加入到链表当中),此处的start会根据不同的系统,调用不同的api,创建好新的线程,在单独执行run,start的执行速度是比较快的,一旦start执行完毕,新线程就会开始执行,调用start的线程,也会继续执行(main),调用start,不一定非得是main线程调用,任何的线程都可以创建其他线程,一个Thread对象只能调用一次start,一个Thread对象只能对应一个线程.

为什么start只能调用一次:设计到线程的状态,在Java中,希望一个Thread对象,只能对应到系统中的一个线程,因此在start中,根据线程状态做出判定,如果Thread对象,是没有start,此时状态时一个new的状态,接下来就可以顺利调用start,如果已经调用过start,就会进入其他状态,只要不是new状态,接下来执行start都会抛出异常

线程的终止
手动设置标志位结束线程

在Java中结束线程,是一个更温柔的操作,不是直接粗暴的,主要怕B线程执行了一半,直接被终止,我们更希望得到的是一个完整品,如果A想让B提前结束,那么就需要让B的run更快速的执行完毕 

这就是线程终止的操作,定义一个成员变量isQuit,然后在main线程中将其置为true即可,引入一个标志位结束线程即可.

变量捕获知识

那么将这个成员变量定义成局部变量能不能行?

显然报错了,这样是不行的,这是因为lambda语法规则--变量捕获

关于变量捕获是lambda/匿名表达式的一个语法规则,lambda可以访问到同一个作用域的所以变量,但是Java的变量捕获有特殊要求,要求捕获的变量必须是final/事实final(什么是事实final呢?就是虽然没有被final修饰但是没有被修改)

为什么写成成员变量就可以,因为lambda本质是函数式接口产生的匿名内部类,走的语法是内部类访问外部类,与变量捕获毫无关系

利用类里面的方法来结束线程

在Thread里有一个成员,就是interrupted Boolean类型的,初始情况下,这个变量是false未被终止的,一旦外面的其他线程调用这个interrupt方法就会设置上述标志位

这个方法是Thread的静态方法,就能获取到调用这个方法的线程的实例,那个线程调用就返回那个线程的实例

在程序运行的过程中不太乐观

抛出了异常 ,由于isInterrupt()和执行打印,这两个操作太快了,大部分时间都是在执行sleep1000这个代码,main调用interrupt的时候,大概率thread还处于sleep的状态中,此处Interrupt不仅仅能设置标志位,还能把刚刚sleep操作给唤醒,比如thread休眠了100ms就被唤醒此时就会catch,并且抛出新的异常,由于新的异常没有人接收,此时我们修改代码

循环并没有停止,看起来好像标志位没有被设置一样,首先Interrupt肯定会设置,其次当sleep等阻塞的函数唤醒之后,会清除刚才设置的Interrupt标志位

如何结束线程呢,由于Java结束线程是"温柔"的,当A希望B结束线程的时候,B收到这样的请求之后,B需自行决定,是否终止,立刻/继续/稍后.(B线程内部的代码自行决定,其他线程无权决定)

        1)如果B线程想无视A,就直接在catch中什么都不做,B线程会继续执行,(sleep清除标志位,就可以使B做出这种决定,如果sleep不清除标志位,B就势必会结束,无法写成继续让线程执行的代码)

        2)如果B想立即结束线程,就直接在catch中写return/break,此时B线程就会立即结束

        3)如果B想稍后在结束,就可以在catch中写入其他的代码(资源释放,清理一些数据等等),这些逻辑写完之后再写return/break

线程等待join()

多个线程,调度顺序是无序的,多个线程谁先执行结束是不确定的,不喜欢结果那么随机,我们更期望让程序的结果是稳定所以,我们可以通过线程等待,就是能够确定线程结束的先后顺序

没写t.join()main和t线程之间的结束顺序是不确定的,如果希望t线程先结束,main后结束,可以在main线程中加入t.join()使用线程等待

join并不影响谁先开始,那个线程调用了join那个线程就被堵塞,main线程不能被join

在main中调用join方法可能会有两种情况

1)t线程已经结束,此时join方法立即返回 

2)如果t线程还没有结束,此时join就会阻塞等待,一直到t线程结束之后,join才能解除阻塞,继续执行

3)如果一个线程还没start就调用join,就被默认成已经完成了,直接返回

阻塞:该线程暂时不参与cpu的调度,解除阻塞继续执行,线程重新回CPU进行调度,

主线程调用多个join,先后顺序无关,总的等待时间是时间较长的那一个,先执行t1.join如果t1还没结束,t1.join就阻塞等待,t1结束之后,main继续执行t2.join,在等待t2结束执行        

main线程调用t1.join,main阻塞等待t1,跟t2没有任何关系,main线程调用t2.join,main阻塞等待t2,跟t1没有任何关系,

新颖的写法只是试验,让t2线程等待main线程

在实际开发中一般都使用join(milise),比如有两个服务器,A中的主线程创建了t线程,t线程给B发送请求,如果B挂了,采用死等的策略,就会时A的主线程被卡住,无法执行后续的逻辑,这种情况就需要制定时间,超过时间不等了,执行下一件事情

线程的状态

关于之前谈到的"进程的状态",更准确的说是"线程的状态",或者叫做"PCB的状态"(Linux系统原生给PCB提供了很多不同的选项,这些都被JVM封装好了)

Java对线程的状态分为六个不同的状态

1.NEW Thread对象有了,还没调用start,系统内部的线程还未创建,

2.TERMINATED 线程已经终止了,内核中的线程已经销毁,Thread对象还在

3.RUNNABLE 指的是线程随叫随到,有两种情况:1.这个线程在CPU上执行2.这个线程没有在CPU上执行,随时可以被调度到CPU上执行,在实际开发过程中,没必要区分这两种情况,因为作为程序员的我们无权干预.

以下三种是阻塞状态

4.WAITING 死等

5.TIMED_WAITING 带超时时间的等

6.BLOCKED 进行锁竞争的时候产生的阻塞

线程状态转化图                 

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值