《javeEE篇》--多线程(1)

avatar
作者
猴君
阅读量:0

进程

在讲线程之前我们先来简单了解一下进程

什么是进程

进程是操作系统对一个正在运行的程序的一种抽象,又或者说,可以把进程看作程序的一次运行过程(通俗的讲就是跑起来的程序)。

而且在操作系统内部,进程是资源分配的基本单位

PCB

PCB的中文翻译是进程控制抽象,在计算机内部要管理任何现实事物,都需要将其抽象成一组有关联的、互为一体的数据。PCB就相当于是对进程的抽象,里面包含了描述一个进程的各种属性,每一个PCB对象就代表着一个进程。

在操作系统中,会有很多进程那么,操作系统对这些进程进行管理,管理的方法是先描述,使用PCB表示出进程的各个属性,后组织,使用数据结构如线性表,搜索树把这些PCB给串起来

 PCB中有一些比较重要的属性

  • pid(进程标识符):用来区分各个进程,是进程的唯一标识符
  • 内存指针:表示进程所在的内存空间,换言之是进程所持有的内存资源
  • 文件描述符表:表示内存所持有的硬盘资源
  • 状态:进程的状态有很多,常见的有运行状态,就绪状态和阻塞状态,运行状态就是进程正在运行,就绪状态就是进程正在准备运行,阻塞状态就是,进程中断,正在等待事件的完成
  • 优先级:不同的进程往往优先级不同,优先级不同往往给进程分配的资源不同,比如当你的电脑一边在打游戏,一边在挂着QQ,QQ的消息可以晚收到一两秒,但是如果游戏里每一个动作都有一两秒的延迟,那么这个游戏就没法打了,所以此时操作系统会给游戏分配更多的资源,不过这个状态在用户眼里往往是不明显的。
  • 上下文:进程执行时寄存器中的数据
  • 记账信息可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等等。

//并发:当我们所要执行的进程太多,cup的核心数不够,就需要让这些进程在cpu上轮流执行,只要轮的够快,在宏观上看起来就像是这些进程在同时执行

线程

线程是什么

我们可以先看一个例子假如一片大空地上有一个厂房,有一天厂长想要加大生产,那么就需要新建工厂,这时有两个选择一个是,再租一篇地皮来建造工厂,另一种是在原有的空地上再建一个

0c2820939ec74f8e9537b863619eb478.png

显然,选择第一种会更加节省开销。

由于进程的创建,销毁等操作开销较大,所以人们提出了线程的概念,进程就相当于是空地,线程就是工厂。线程相当于是进程的一个执行路径,也可以叫做“轻量级的进程”。同一个进程中的线程会共享进程所申请到的资源,所以创建线程时不需要再额外申请空间,这样就大大降低了调度的成本。进程有的一些属性,线程往往也具有。

线程是包含在进程内的,这样一个进程会有多个PCB同时表示,每个PCB就用来代表一个线程,每个线程都有自己的状属性(状态,优先级,上下文......),每个线程都可以独立的去CPU上调度执行,这些PCB共用了同样的内存指针和文件描述表,这就使创建线程(PCB)就不需要重新申请空间了,就大大提高了创建和销毁线程的效率。

 线程和进程的区别

  • 进程是资源分配的基本单位,线程是执行调度的基本单位
  • 进程包含线程,一个进程至少会有一个线程,这个至少的线程叫做主线程
  • 同一个进程的线程之间,共用同一份资源(内存+硬盘),省去了申请资源的开销
  • 进程和进程之间是互相独立的,进程和线程之间,可能会互相影响
  • 进程和线程都是用来实现并发场景的,但是线程比进程更加轻量,更高效

线程的创建

方法一

继承Thread,重写run:

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

  1. 首先定义一个类(这里我的类名为MyThread),这个类需要继承Thread
  2. 然后需要重写run方法,run方法内部就是我们要执行的线程代码
  3. 最后启动线程
class MyThread extends Thread{     public void run(){         while (true){             System.out.println("hello thread");             try {                 Thread.sleep(1000);                 //因为父类的抽象方法没有抛出异常,所以这里只能try catch             } catch (InterruptedException e) {                 throw new RuntimeException(e);             }         }     } } public class Main {     public static void main(String[] args) throws InterruptedException {         Thread thread = new MyThread();         thread.start();         while(true){             System.out.println("hello world");             Thread.sleep(1000);             //这里不是继承自父类         }     } }

 注意此处我们调用的不是run方法而是start方法,如果只是单纯的调用run方法是不会启动线程的,run方法不会分配新的分支栈。

start方法的作用是,启动一个分支栈,通过调用系统的API,在JVM中创建一个新的栈空间,来在系统内核中创建线程,而run方法就只是单纯的描述一下这个线程要执行啥内容,run方法会在start方法创建好线程,线程启动成功之后自己被调用。

方法二

实现Runnable接口,重写run

  1. 定义一个类实现Runnable接口
  2. 实现run方法
  3. 构建Thread对象,将创建的Runnable对象作为参数传入
  4. 启动线程
class MyRunnable implements Runnable{     @Override     public void run() {         while (true) {             System.out.println("hello runnable");             try {                 Thread.sleep(1000);             } catch (InterruptedException e) {                 throw new RuntimeException(e);             }         }     } } public class Demo1 {     public static void main(String[] args) throws InterruptedException {         Runnable runnable = new MyRunnable();         Thread thread = new Thread(runnable);         thread.start();         while (true){             System.out.println("hello main");                 Thread.sleep(1000);         }     }  }

//这里Runnable表示一个可执行的任务,它将这个任务交给线程负责执行 

方法三

匿名内部类

可以不用单独创建一个类直接使用匿名内部类

  •  使用匿名类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象 Thread t1 = new Thread() {     @Override     public void run() {         System.out.println("使用匿名类创建 Thread 子类对象");    } }; 
  •  使用匿名类创建 Runnable 子类对象 
// 使用匿名类创建 Runnable 子类对象 Thread t2 = new Thread(new Runnable() {     @Override     public void run() {         System.out.println("使用匿名类创建 Runnable 子类对象");    } }); 
  • 使用lambda 表达式创建 Thraed子类对象

 lambd表达式相当于是匿名内部类的替换写法,这种方法可以快速方便的就创建出一个线程

public class Demo4 {     public static void main(String[] args) throws InterruptedException {         Thread thread = new Thread(() -> {             while (true){                 System.out.println("hello Thread");                 try {                     Thread.sleep(1000);                 } catch (InterruptedException e) {                     throw new RuntimeException(e);                 }             }         });         thread.start();         while (true){             System.out.println("hello main");             Thread.sleep(1000);         }     } }

//lambda表达式本质上,是一个匿名函数(没有名字的函数,用一次就完了),主要来实现“回调函数”的效果

Thread 类及常见方法 

Thread 的常见构造方法

8090e419b1fe4f09a4ecc510f02e463d.png

 我们在创建线程的时候可以给线程取名字,线程取名字不会影响到线程的正常运行,只是方便之后的区分,可以在jdk给我们提供的工具jconsole.exe查看

//还可以使用setName方法手动命名

Thread 的几个常见属性

e931ca623c13423caf6741c60303402a.png

//在默认情况下一个线程是前台线程,一个Java进程中如果前台线程没有执行结束,此时整个进程是一定不会结束的,后台线程(守护线程),不结束不会影响到整个进程的结束

 af657da915f248108f4186582ae5782e.png

执行结果

cec7b66b3c004284a1390912bd4f15da.png

改成后台线程之后主线程飞快地执行完了,此时没有其他前台线程了,于是进程结束,t线程来不及执行就完了

使用isAlive()可以知道当前线程是否在执行,如果在执行就会返回true,否则返回false

线程控制

休眠当前线程sleep

sleep可以让当前线程停止一定之间

b4eec42cbffd4313be85b085b3c581db.png

//因为父类的抽象方法没有抛出异常,所以这里只能try catch

运行结果:

0bb94582c8c84e169804a513d11fe4e5.png

但是要注意,因为线程的调度是不可控的,所以,这个方法只能保证实 际休眠时间是大于等于参数设置的休眠时间的。

b63a7119317a4648a2f2efe90ee60dee.png

fdce1744139249eda0734d2bdbff19d4.png

线程中断interrupt

常见的线程中断方式有两种

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

1.使用自定义的变量来作为标志位

我们定义一个当作线程中断标志的变量,通过其他线程对这个变量的修改,来实现对该线程的中断 

494391cba8354ca9bdc565b1049e76fe.png

但是这种方法显然看起来有些简陋,而且如果使用lambda表达式创建线程会比较麻烦,而且如果线程内部在sleep的时候,主线程修改变量,新线程内部不能及时响应

lambda表达式会自动捕获方法内,之前出现的变量

lambda表达式内使用的标志,必须是final或者常量

 2.使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

 在Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记,叫做中断标志位。

  • interrupt方法可以中断对象关联的线程,如果此时线程处在阻塞状态下比如wait/join/sleep,interrupt就会报出一个异常,否则将会设置标志位(把Thread对象内部的标志位设置为true)
  • Thread.interrupted() 可以判断,当前线程的中断标志位是否被设置,如果被设置就会返回true否则返回false,并且在调用结束后,会清除标志位,比如当interrupt将标志位设置为true时,Thread.interrupted()就会先返回true然后再将刚刚被设置的标志位清除(将标志位再变成false),就好像一个按钮,按一下就会会弹起来。
  • Thread.currentThread().isInterrupted()也可以判断当前对象的线程的标志位是否被设置,但是调用后不清除标志位,比如当interrupt将标志位设置为true,Thread.currentThread().isInterrupted()只会返回一个true之后什么也不会做了,就像一个拉杆,拉一下会持续有效。

 //注意interrupt并不能直接中止线程,他的作用只是设置对象里的标志位,我们可以通过这个标志位来间接的中断线程,之所以这样是为了可以让程序猿有更大的操作空间来决定是否要中断线程。

举例:

a67fe02a91f94f298aabab14c1cacce9.png

我们刚刚有说到当调用interrupt时,如果此时线程处在阻塞状态下比如wait/join/sleep,interrupt就会报出一个异常,所以当出现interruptException时,要不要直接结束线程,或者执行一段代码后再结束比如收尾工作,又或者是直接忽略这个异常,就取决于我们catch中的写法了

补充:

currentThread()的作用是那个线程调用这个方法,就会返回那个线程的对象,所以Thread.currentThread()就相当于,获取到当前的线程实例,在这里就是thread

运行结果:

879683a00e494255ac6e85cbf5c65ce0.png

运行后我们发现线程并没有停止,刚刚我们说过Thread.currentThread().isInterrupted()不会清理标志位,按理来说当执行interrupt时标志位被改,Thread.currentThread().isInterrupted()返回true,线程应该执行结束了呀?

上述结果异常确实是出现了,sleep也确实被唤醒了,但是线程任然在工作。在interrupt唤醒线程之后,此时seelp方法抛出异常,在抛出异常的同时还会顺带自动清理刚才设置的标志位,所以这里标志位并不是被Thread.currentThread().isInterrupted()清理的,这样就使interrupt的“设置标志位”的效果看起来就好像没生效一样。

线程等待join

线程等待就是,让一个线程等待另一个线程执行结束,再继续执行,本质上就是控制线程结束的顺序。利用join实现线程等待

5543126726f44acf9920e0272273b124.png

t.join意思就是,当前线程等待t线程执行结束之后才可以执行,那个线程调用的join,那个线程就需要等待

  • 如果t线程正在运行中,此时调用join的线程main就会阻塞,一直阻塞到t线程执行结束为止
  • 如果t线程已经执行结束,此时调用join线程,就会直接返回了,不会涉及阻塞

//但是有时如果让线程一直等待下去,也不太合适所以我们往往会设定一个,最大等待时间,如果超出这个时间就会停止等待

4f156a56221e48e9a236910150f371a2.png

线程状态

线程的状态其实,是一个枚举类型,可以通过sout打印。

  • NEW:Thread对象已经有了,但是线程还没有启动(start方法还没调用)
  • RUNNABLE:就绪状态,线程已经在CPU上执行了/线程线程正在等待CPU调度
  • TIMED_WAITING:阻塞状态,由于sleep这种固定时间的方式发生的阻塞
  • WAITING:阻塞状态,由于wait这种不固定时间的方式产生的阻塞(会在之后的篇章中讲到)
  • BLOCKED:阻塞,由于锁竞争导致的阻塞(会在之后的篇章中讲到)
  • TERMINATED:对象还在,内核中的线程以及没了(线程执行完了)

 我们可以通过getState来获取当前状态

e311f88a45834d03b7512e5586a19e72.png

运行结果:

5dc2f733e2974f47af9f32ea6ee08d6c.png

以上就是博主对线程知识的分享,在之后的博客中会陆续分享有关线程的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望多多支持博主之后和博客!!🥰🥰

下一篇博客博主将分享有关线程安全以及锁等知识,还希望多多支持一下!!!😊

 

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!