在讲线程之前我们先来简单了解一下进程
进程是操作系统对一个正在运行的程序的一种抽象,又或者说,可以把进程看作程序的一次运行过程(通俗的讲就是跑起来的程序)。
而且在操作系统内部,进程是资源分配的基本单位
PCB的中文翻译是进程控制抽象,在计算机内部要管理任何现实事物,都需要将其抽象成一组有关联的、互为一体的数据。PCB就相当于是对进程的抽象,里面包含了描述一个进程的各种属性,每一个PCB对象就代表着一个进程。
在操作系统中,会有很多进程那么,操作系统对这些进程进行管理,管理的方法是先描述,使用PCB表示出进程的各个属性,后组织,使用数据结构如线性表,搜索树把这些PCB给串起来
PCB中有一些比较重要的属性
- pid(进程标识符):用来区分各个进程,是进程的唯一标识符
- 内存指针:表示进程所在的内存空间,换言之是进程所持有的内存资源
- 文件描述符表:表示内存所持有的硬盘资源
- 状态:进程的状态有很多,常见的有运行状态,就绪状态和阻塞状态,运行状态就是进程正在运行,就绪状态就是进程正在准备运行,阻塞状态就是,进程中断,正在等待事件的完成
- 优先级:不同的进程往往优先级不同,优先级不同往往给进程分配的资源不同,比如当你的电脑一边在打游戏,一边在挂着QQ,QQ的消息可以晚收到一两秒,但是如果游戏里每一个动作都有一两秒的延迟,那么这个游戏就没法打了,所以此时操作系统会给游戏分配更多的资源,不过这个状态在用户眼里往往是不明显的。
- 上下文:进程执行时寄存器中的数据
- 记账信息可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等等。
//并发:当我们所要执行的进程太多,cup的核心数不够,就需要让这些进程在cpu上轮流执行,只要轮的够快,在宏观上看起来就像是这些进程在同时执行
我们可以先看一个例子假如一片大空地上有一个厂房,有一天厂长想要加大生产,那么就需要新建工厂,这时有两个选择一个是,再租一篇地皮来建造工厂,另一种是在原有的空地上再建一个
显然,选择第一种会更加节省开销。
由于进程的创建,销毁等操作开销较大,所以人们提出了线程的概念,进程就相当于是空地,线程就是工厂。线程相当于是进程的一个执行路径,也可以叫做“轻量级的进程”。同一个进程中的线程会共享进程所申请到的资源,所以创建线程时不需要再额外申请空间,这样就大大降低了调度的成本。进程有的一些属性,线程往往也具有。
线程是包含在进程内的,这样一个进程会有多个PCB同时表示,每个PCB就用来代表一个线程,每个线程都有自己的状属性(状态,优先级,上下文......),每个线程都可以独立的去CPU上调度执行,这些PCB共用了同样的内存指针和文件描述表,这就使创建线程(PCB)就不需要重新申请空间了,就大大提高了创建和销毁线程的效率。
- 进程是资源分配的基本单位,线程是执行调度的基本单位
- 进程包含线程,一个进程至少会有一个线程,这个至少的线程叫做主线程
- 同一个进程的线程之间,共用同一份资源(内存+硬盘),省去了申请资源的开销
- 进程和进程之间是互相独立的,进程和线程之间,可能会互相影响
- 进程和线程都是用来实现并发场景的,但是线程比进程更加轻量,更高效
继承Thread,重写run:
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
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
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 t1 = new Thread() { @Override public void run() { System.out.println("使用匿名类创建 Thread 子类对象"); } };
// 使用匿名类创建 Runnable 子类对象 Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("使用匿名类创建 Runnable 子类对象"); } });
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表达式本质上,是一个匿名函数(没有名字的函数,用一次就完了),主要来实现“回调函数”的效果
我们在创建线程的时候可以给线程取名字,线程取名字不会影响到线程的正常运行,只是方便之后的区分,可以在jdk给我们提供的工具jconsole.exe查看
//还可以使用setName方法手动命名
//在默认情况下一个线程是前台线程,一个Java进程中如果前台线程没有执行结束,此时整个进程是一定不会结束的,后台线程(守护线程),不结束不会影响到整个进程的结束
执行结果
改成后台线程之后主线程飞快地执行完了,此时没有其他前台线程了,于是进程结束,t线程来不及执行就完了
使用isAlive()可以知道当前线程是否在执行,如果在执行就会返回true,否则返回false
sleep可以让当前线程停止一定之间
//因为父类的抽象方法没有抛出异常,所以这里只能try catch
运行结果:
但是要注意,因为线程的调度是不可控的,所以,这个方法只能保证实 际休眠时间是大于等于参数设置的休眠时间的。
常见的线程中断方式有两种
1.使用自定义的变量来作为标志位
我们定义一个当作线程中断标志的变量,通过其他线程对这个变量的修改,来实现对该线程的中断
但是这种方法显然看起来有些简陋,而且如果使用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并不能直接中止线程,他的作用只是设置对象里的标志位,我们可以通过这个标志位来间接的中断线程,之所以这样是为了可以让程序猿有更大的操作空间来决定是否要中断线程。
举例:
我们刚刚有说到当调用interrupt时,如果此时线程处在阻塞状态下比如wait/join/sleep,interrupt就会报出一个异常,所以当出现interruptException时,要不要直接结束线程,或者执行一段代码后再结束比如收尾工作,又或者是直接忽略这个异常,就取决于我们catch中的写法了
补充:
currentThread()的作用是那个线程调用这个方法,就会返回那个线程的对象,所以Thread.currentThread()就相当于,获取到当前的线程实例,在这里就是thread
运行结果:
运行后我们发现线程并没有停止,刚刚我们说过Thread.currentThread().isInterrupted()不会清理标志位,按理来说当执行interrupt时标志位被改,Thread.currentThread().isInterrupted()返回true,线程应该执行结束了呀?
上述结果异常确实是出现了,sleep也确实被唤醒了,但是线程任然在工作。在interrupt唤醒线程之后,此时seelp方法抛出异常,在抛出异常的同时还会顺带自动清理刚才设置的标志位,所以这里标志位并不是被Thread.currentThread().isInterrupted()清理的,这样就使interrupt的“设置标志位”的效果看起来就好像没生效一样。
线程等待就是,让一个线程等待另一个线程执行结束,再继续执行,本质上就是控制线程结束的顺序。利用join实现线程等待
t.join意思就是,当前线程等待t线程执行结束之后才可以执行,那个线程调用的join,那个线程就需要等待
//但是有时如果让线程一直等待下去,也不太合适所以我们往往会设定一个,最大等待时间,如果超出这个时间就会停止等待
线程的状态其实,是一个枚举类型,可以通过sout打印。
我们可以通过getState来获取当前状态
运行结果:
以上就是博主对线程知识的分享,在之后的博客中会陆续分享有关线程的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望多多支持博主之后和博客!!🥰🥰
下一篇博客博主将分享有关线程安全以及锁等知识,还希望多多支持一下!!!😊