当前位置: 首页 > news >正文

JavaEE--2.多线程

1.认识线程(Thread)

1.1概念

        1)什么是线程

        ⼀个线程就是⼀个 "执行流". 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行着多份代码.

        2)为什么要有线程

        首先, "并发编程" 成为 "刚需".

                1.   单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU 资源.

                2. 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程

        其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量

         创建线程比创建进程更快.

         销毁线程比销毁进程更快.

         调度线程比调度进程更快.

        最后, 线程虽然比进程轻量, 但是⼈们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程" (Coroutine)

多进程的问题就是太"重"

        创建/销毁进程的开销比较大(时间,空间)

        一旦需求常见,需要频繁地创建销毁进程,开销非常明显.

        最典型的例子就是服务器的开发:针对每个发送请求的客户端都创建一个单独的进程,由这个进程负责给客户端进行服务

为了解决进程开销比较大的问题,发明了"线程"(Thread)

                               线程可以理解成更轻量的进程,也能实现并发编程,创建/销毁的开销比进程更低.

因此,多线程的编程就成了当下主流的并发编程方式.

 

        所谓的进程,在系统中是以PCB(进程控制块)这样的结构体来描述的,通过链表的形式来组织的

        对于系统中,线程同样也是通过PCB来描述的(Linux)

        一个进程其实是一组PCB

        一个线程是一个PCB

        所以,其中存在"包含关系”,一个进程中,可以包含多个线程

        此时每个线程都可以独立的到CPU上调度执行

        线程是系统“调度执行”的基本单位

        进程是系统“资源分配”的基本单位

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

        上个文章提到的"进程调度"相当于"线程调度",针对"只包含一个线程的进程来说"

        如果有多个线程在一个进程中,每个线程,都会有自己的状态,优先级,上下文,记账信息,每个都会各自独立的在CPU上调度执行

        进程要么包含一个线程,要么包含多个线程,不能没有线程

        同一个进程的这些线程,共用着同一个系统资源

        线程比进程更轻量,主要就在与创建线程省去了"分配资源"过程,销毁线程也省去了"释放资源"过程.

分配资源这个操作的开销大

        一旦创建进程,同时也会创建第一个线程=>就会负责分配资源

        一旦后续创建第二个第三个线程,就不必再重新分配资源了....

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

        一个核心,也可以按照"分时复用",来切换多个线程,从微观上来看.多个线程是"一个接一个"的执行的,由于调度速度足够快,宏观上看起来好像"同时执行",这时称之为"并发"

        提升效率的关键是在于充分利用多核心进行"并行执行"

        如果只是"微观并发",速度是没有提升的

        真正能提升速度的是"并行"

线程有关的问题:

1.线程数目太多

        如果线程数目太多,比如超出了CPU核心数目.此时就无法在微观上完成"并行"执行,势必会存在严重的"竞争".

2.进程资源"冲突"

        当线程数目多了之后,此时就容易发生"冲突",由于多个线程,使用的是同一份资源(内存资源)

        内存资源指的是代码中定义的变量/对象,如果多个线程同时对一个变量进行读写操作(尤其是写操作)容易发生冲突

        一旦发生冲突,就可能使程序出现问题("线程安全问题"[重点内容,难点内容])

3.线程异常

        当一个进程中有多个线程的时候,一旦某个线程抛出异常,这个时候,如果未能妥善处理,可能导致整个进程崩溃,其他线程就会随之崩溃

总结下(这也是高频的面试题)

进程线程的概念和区别

        1.进程包含线程

一个进程里可以有一个线程,也可以有多个线程,不能没有线程

        2.进程是系统资源分配的基本单位

线程是系统调度执行的基本单位

        3.同一个进程里的线程之间,共用着同一份系统资源(内存,硬盘,网络带宽等...)

尤其是"内存资源",就是代码中定义的变量/对象...

                                        编程中,多个线程,是可以共用同一份变量的

        4.线程是当下实现并发编程的主流方式,通过多线程,就可以充分利用好多核CPU

        但是不是线程数目越多越好,线程数目达到一定程度,把多个核心都充分利用了之后,此时继续增加线程无法再提高效率,甚至可能会影响效率(线程调度也是有开销的)

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

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

3)进程和线程的区别

        进程是包含线程的,每个进程至少有一个线程,即主线程.

        进程和进程之间不共享内存空间,同一进程的线程之间共享同一个内存空间

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

        一个进程挂了一般不会影响到其他进程,一个线程挂了可能会把同进程的其他线程一起带走(整个进程崩溃).

4)Java的线程和操作系统线程的关系

        线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户提供了一些API供用户使用(例如Linux的pthread库).

        Java标准库中的Thread类可以视为对操作系统提供的API进行了进一步的抽象和封装.

        Java(JVM)把这些系统API封装好了,咱们不需要关注系统原生API,只需要了解好Java提供的这一套API就行了


 

1.2第一个多线程程序

Thread标准库:

        这个类就负责完成多线程相关的开发.

此处Thread类可以直接使用,不需要导入任何的包

(因为有的类比较特殊,默认已经导入了,比如String)

此处的继承不是主要目的,更主要的目的是为了重写run方法

        调用start就会在进程内部创建出新的线程,新的线程就会执行刚才run里面的代码

  上述代码没有run方法没有手动的去调用但是最终也是执行了像run这种用户手动定义,但是没有用户手动调用,最终这个方法被系统/库/框架进行调用了此时,这样的方法被称为"回调函数"(callback)回调函数是编程中一个非常重要的概念

回调函数是一个非常重要的概念!!

回调函数概念其实目前至少遇到过两次

1.C语言中的函数指针

        函数指针主要有两个用途

        a)作为回调函数

        b)实现转移表--降低代码的复杂程度

2.Java数据结构中的优先级队列

        优先级队列必须先定义好对象的"比较规则"     Comparable   Compareto

                                                                                Comparator     Compare        

                自己定义了这些比较规则,但是并没有调用,此时都是标准库本身内部的逻辑负责调用

       

        这个代码运行起来,是一个进程,但是这个进程实际上是包含了两个线程!

        调用main方法的线程称为"主线程"

        一个进程至少得包含一个线程,至少这一个线程就是主线程

        t.start() 这里又手动的创建了新的线程

         主线程和新线程都并发/并行的在CPU上执行

        Thread,sleep();是Thread的静态方法   类名.方法名

        Java标准库提供的方法

        C语言中也有类似的,Sleep()    (Windows的系统函数)

        多线程之间谁先去CPU上调度执行,这个过程是"不确定的"(不是数学意义上的随机)

        而是这个调度顺序取决于操作系统,内核里"调度器"的实现

        调度器里有一套规则,咱们作为应用程序开发没办法进行干预,也感受不到

        只能将这个过程近似的视为"随机"                                          "抢占式执行"

借助第三方工具更直观的来看这两个线程的情况

在jdk的bin目录中打开jconsole

可以看出当前java进程中线程的情况,左下角可以看出一个java进程中不只有两个线程,还有别的

main是主线程,调用main方法的

Thread-0是代码中自己创建的线程,它的命名规律是Thread-数字


在IDEA的调试器,也能看到类似的信息

        

就可以手动的切换都某个线程上,看你需要关注的一些信息

创建线程的写法

1)继承Thread,重写run

上面的写法就是继承Thread类

1.继承 Thread 来创建⼀个线程类.

class MyThread extends Thread {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");}
}

2.创建 MyThread 类的实例

MyThread t = new MyThread();

3.调用 start 方法启动线程

t.start();

全部代码:

2)实现Runnable接口,重写run

1.实现 Runnable 接口

class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");}
}

2.创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.

Thread t = new Thread(new MyRunnable());

3.调用 start 方法

t.start();

全部代码:

        Runnable就是用来描述"要执行的任务"  是什么

        有人认为Runnable这种做法更有利于"解耦合",这只是一个任务,并不是和"线程"这样的概念强相关

        后续执行这个任务的载体可以是线程也可以是别的

        别的在这里比如后续会介绍的线程池来执行任务

        再比如可以通过"虚拟线程"(协程)来执行~~

        线程是轻量级进程,因为进程太重量了,随着对于性能要求进一步提高,开始嫌弃线程也太重量了

        于是引入了"协程".(轻量级线程,纤程)

        这个协程的概念在后续的java版本中就被引入了,起的名叫做"虚拟线程"

3)内部匿名类

内部匿名类没有类目

        Runnable子类对象

Thread子类对象

1.定义匿名内部类,这个类是Thread的子类

2.类的内部重写的父类run方法

3.创建了一个子类的实例,并且把实例的引用赋值给了t

匿名内部类就是一次性使用的类,用完就丢

4.lambda表达式创建Runnable子类

 Thread thread=new Thread(() -> {while(true){System.out.println("这是线程输出的");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();while(true){System.out.println("这是main运行的代码");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}

        这也是推荐的写法

        后续多以lambda表达式来创建多线程

2.1Thread的常见构造方法

方法说明
Thread()创建线程对象
Tread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Tread(Runnable target,String name)使用Runnable对象创建线程对象,并命名
[了解]Thread(ThreadGoup goup,Runnable target)线程可以被用来分组管理,方便统一的设置线程的一些属性

注:给线程起名字不会影响到线程的执行效果,但是有一个合适的名字有利于调试的结果

不起名字默认就是Thread-0,Thread-1...

        现在很少会使用线程组,线程的相关属性用的也不多,现在更多的是会使用线程池

2.2Thread的几个常见的属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

1.ID是线程的唯一标识,不同的线程不会重复

        JVM自动分配的,Thread对象的身份标识

2.名称是各种调试工具用到的

3.状态表示线程当前所处的一个情况,下面我们会具体说明

        就绪/阻塞状态                     就绪状态的名字为RUNNABLE

        通常情况下,一个Thread对象,就是对应到系统内部的一个线程(PCB),但是也可能会存在一个情况,Thread对象存在,但是系统内部的线程已经没了/还没有创建

         

4.优先级高的线程理论上来说更容易被调度到

        设计不同的优先级影响系统的调度,这里的影响是基于"统计"规则的影响,直接肉眼观察河南观察到效果

5.关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后才会结束运行

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

        前台线程:如果某个线程在执行的过程中,能够阻止进程的结束,这个线程就是"前台进程 "

6.是否存活,即简单的理解run的方法是否结束了

        true为存在,false为无了

由于主线程和thread线程并发执行,主线程执行上述4个操作,执行了一部分的时候thread线程就执行打印了

RUNNABLE正在运行,实际上Java没有RUNNABLE这个线程状态,把正在CPU上运行和随时调度到CPU上运行的都统称为RUNNABLE

后台线程:

        1)进程要结束(前台线程要结束),无力阻止

        2)后台线程先结束也不影响进程的结束(其他前台进程的结束)

前台进程:

        1)前台线程结束,此时进程就结束,后台线程也就随之结束

        2)前台线程不结束,后台线程结束了不影响

         一个进程中,前台线程可以有很多个(创建的线程默认就是前台的),必须所有的前台线程都结束,进程才能结束

        代码中,创建的new Thread对象生命周期和内核中的实际的线程是不一定一样的

        可能会出现Thread对象仍然存在,但是内核中的线程不存在的情况但是不会出现(Thread对象不存在,线程还在的情况)

        1.调用start之前,内核中还没创建线程

        2.线程的run执行完毕,内核的线程就没了,但是Thread对象仍然存在

2.3启动一个线程

        调用start方法,才真正的在操作系统的底层创建出一个线程.

        经典问题:start和run之间的区别

        start:调用系统函数,真正的在操作系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调出不同的api(windows,linux,mac...),创建好新的线程之后再单独执行run

        run:描述了线程要执行的的任务,也可以成为"线程的入口"

        start的执行速度一般是比较快的(创建线程 比较轻量)

        一旦start执行完毕,新线程就会开始执行,调用start的线程也会继续执行main

        两个线程分别个做个的

        调用start不一定非得是main线程调用的,任何线程都可以创建其他线程

如图:这里就是由thread调用了thread1线程

        如果系统资源充裕,就可以任意的创建线程,(当然,线程不是越多越好);

       

        一个Thread对象只能调用一次start,如果多次调用start就会出现问题(一个Thread对象只能对应系统中的一个线程)

IllegalThreadStateException 报错

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

        

2.4程序的中断(程序的终止)

目前常见的有以下的两种方法:

1.通过共享的标记来进行沟通

2.调用interrupt()方法来停止

        例如:A,B两个线程,B正在运行,A想让B结束,核心在于A让B的run方法直接完毕,此时B就自然结束了,而不是说B的run执行一般,A直接把B强制结束了

用共享标记结束,例如:

    private static boolean isQuit=false;public static void main(String[] args) throws InterruptedException {Thread thread=new Thread(()->{while(!isQuit){System.out.println("hello thread");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();Thread.sleep(3000);System.out.println("main方法开始中断程序");isQuit=true;}

用isQuit来作为标志位

如果将isQuit放在main方法里面,作为局部变量,代码会报错

变量捕捉,是lambda表达式/匿名内部类的一个语法规则

        isQuit和lambda定义在一个作用域中,此时lambda是可以访问到lambda外部(和lambda同一个作用域的变量),Java的变量捕捉有特殊要求,要求捕捉的变量得是final/事实final

        写成成员变量之后就可以了是因为此时走的语法是"内部类访问外部类的成员"

        lambda表达式本质上是一个"函数式接口"产生的"匿名内部类"

        

2.调用interrupt来终止

1)interrupt方法能够设置标志位,也能唤醒sleep等阻塞的方法(会抛出异常)

2)sleep被唤醒后,会清空标志位

如图可以看出,标志位被sleep清空,并没有中断此线程

线程A和线程B,A希望B线程终止

1)如果B线程想无视A线程,就直接catch中啥也不做

B线程仍然会继续执行      

sleep清除标志位,就可以使B能做出这种选择,如果sleep不清楚标志位,B势必会结束,无法写出继续让线程继续的代码

2)如果B线程想立即结束就直接在catch中写上return或者break

此时B线程就会立即结束

3)如果B想稍后再结束,就可以在catch中写上一些其他的逻辑(比如释放资源,清除硬一些数据,提交一些结果....收尾工作)

这些逻辑完成之后再进行return/break;

这些给了程序员更多的操作空间

2.5等待一个线程-join()

操作系统针对多个线程的执行是一个"随机调度,抢占式执行"的过程

线程等待就是在确定两个线程的结束顺序

无法确定两个线程调度执行顺序,但是可以控制谁先结束,谁后结束

        让后结束的线程等待先结束的线程即可,此时后结束的线程会进入阻塞,一直到先结束的线程真的结束了,阻塞才结束

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,醉倒等millis毫秒

public boid join(long millis,int nanos)

一般不用,后面是精确到纳秒

同理,但可以更高精度

因为计算机很难做到精确计时,一般能精准到ms就不错了

和操作系统相关像windows,linux系统,线程调度开销比较大,操作系统中还有一类系统"实时操作系统"就能把调度开销尽可能降低,开销小于一定的误差要求,从而可以做到更精准

        

 Thread thread1=new Thread(()->{for(int i=0;i<3;i++){System.out.println("Hello thread1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2=new Thread(()->{for(int i=0;i<4;i++){System.out.println("Hello thread2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println("main开始");thread1.start();thread2.start();try {thread1.join();} catch (InterruptedException e) {e.printStackTrace();}try {thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main结束");

此代码为例,就是main线程等待thread1和thread2结束后才往下运行

当然不一定是main去等待,thread1也可以去等待thread2结束后再继续

代码如下

 public static void main(String[] args) {Thread thread1=new Thread(()->{for(int i=0;i<3;i++){System.out.println("Hello thread1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2=new Thread(()->{try {thread1.join();} catch (InterruptedException e) {e.printStackTrace();}for(int i=0;i<4;i++){System.out.println("Hello thread2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println("main开始");thread2.start();thread1.start();try {thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main结束");}

这里就是thread2中等待thread1

2.6获取当前线程的引用

方法说明
public static Thread currentThread();返回当前线程对象的引用

2.7休眠当前线程

线程执行sleep,就会使得这个线程不参与CPU调度,从而把资源让出来给别人使用

                                        "放权"

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程
public static void sleep(long millis,int nanos) throws InterruptedException可以更高精度精确到纳秒

3.线程的状态

进程的状态:

就绪:正在CPU上执行,或者随时可以去CPU上执行

阻塞:暂时不能去参与CPU执行

Java的线程对于状态做了更详细的区分,不仅仅是就绪和阻塞了

六种状态

1.NEW                                 这种Thread对象虽然有了,但是内核的线程还没有(还没有调用start)

 public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {for (int i=0;i<3;i++){System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println(thread.getState());thread.start();
}

2.TERMINATED                  当前Thread对象虽然还在,但是内核的线程已经销毁(线程已经结束了)

    public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {for (int i=0;i<3;i++){System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(thread.getState());}

3.RUNNABLE                      就绪状态.正在CPU上运行+随时可以去CPU上去运行

public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {while(true){}});thread.start();System.out.println(thread.getState());}

4.BKLOCKED                      因为锁竞争引起的阻塞

   public static void main(String[] args) throws InterruptedException {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread1=new Thread(() -> {synchronized (Locker) {for (int i = 0; i < 50000; i++) {count++;}}});Thread thread2=new Thread(()->{synchronized(Locker){for(int i=0;i<50000;i++) {System.out.println(thread1.getState());count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count);}

5.TIMED_WAITING              有超时时间的阻塞等待,比如sleep或者join带参数版本

public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {while(true){System.out.println("hello thread");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(thread.getState());}

6.WAITING                            没有超时时间的阻塞等待,join/wait

    public static void main(String[] args) throws InterruptedException {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {while(true){}});Thread thread2=new Thread(()->{try {thread.start();thread.join();} catch (InterruptedException e) {e.printStackTrace();}});thread2.start();Thread.sleep(3000);System.out.println(thread2.getState());}

        学习线程状态主要就是为了调试,比如遇到某个代码功能没有执行就可以观察对应线程的状态,看看是否是因为一些原因阻塞了.

相关文章:

  • 把dll模块注入到游戏进程的方法_基于文件修改的注入方式
  • MCP:AI时代的“万能插座”,开启大模型无限可能
  • SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
  • 进程与线程:02 多进程图像
  • 在统信UOS 1060上实现自动关机
  • 高防IP能抵御哪些类型的网络攻击?
  • Buildroot、BusyBox与Yocto:嵌入式系统构建工具对比与实战指南
  • 辛格迪客户案例 | 苏州富士莱医药GMP培训管理(TMS)项目
  • 深度学习3.3 线性回归的简洁实现
  • XXL-JOB 深入理解教程
  • 【MySQL】表的约束(主键、唯一键、外键等约束类型详解)、表的设计
  • javaSE.二叉查找树和平衡二叉树
  • EMQX安装使用和客户端认证
  • PCIE Spec ---Base Address Registers
  • 13 数据存储单位与 C 语言整数类型:从位到艾字节、常见整数类型及其范围、字面量后缀、精确宽度类型详解
  • 【嵌入式系统设计师(软考中级)】第二章:嵌入式系统硬件基础知识(上)
  • 玩转Docker | 使用Docker部署nullboard任务管理工具
  • 基于Python的图片/签名转CAD小工具开发方案
  • 数字IC后端PR阶段Innovus,ICC,ICC2修复short万能脚本分享
  • Sunscreen的TFHE 与Parasol编译器新愿景
  • 一季度减持阿里、美团,加仓顺丰,张坤:与其纠结经济,不如着眼企业
  • 工人日报评一些旅行社不收记者律师:“拒客黑名单”暴露心虚病
  • 对话地铁读书人|财务管理孟先生:老婆让我看《三体》
  • 北京媒体锐评男子地铁辱骂他人:北京地铁永远欢迎沾着泥巴的普通劳动者
  • 韩国新一届总统选举将于6月3日举行,民调显示李在明继续领跑
  • 累计亏损10亿元,桂林旅游怎么了?