📎前言
✨本篇文章是博主首次学习多线程相关内容而总结内容相关的博客,希望大家可以提供宝贵的意见,我定会积极采纳~ 后续我也将持续更新自己的学习记录,以此来督促我不断地学习与进步.希望大家关注支持!✨
🚀🚀系列专栏:【多线程】
📻📻本章内容:初识多线程
📌进程
进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做运行中的程序;
- 在操作系统内部,进程又是操作系统进行资源分配的基本单位。
- 每一个进程都有属于自己的存储空间与系统资源.
🍟进程的管理
1.先描述
使用PCB结构体表示进程内的各种属性
2.再组织
使用双向链表数据结构,把多个PCB结构体串起来
🍔PCB包含的属性
PCB指的是进程控制块(Process Control Block),它是操作系统用来管理进程的数据结构,包含了进程的各种信息,如进程状态、程序计数器、寄存器、内存分配情况、打开文件列表等。PCB包含了以下的属性以达到进程控制的目的.
- PID:进程的身份标识
- 内存指针
- 文件描述符表(操作文件就是操作硬盘)
- 进程的状态
- 进程的优先级
- 进程的上下文(中间状态存放到寄存器)
- 进程的记账信息(统计各进程调用多久了)
🍅并行与并发
- 并行:同一时刻,两进程同时运行在两个CPU逻辑核心上
- 并发:两进程在同一核心上交替着上
由于CPU切换进程速度过快,微观上是串行的。宏观上看来这两进程就是同时进行的.因此二者通常统称为并发.
📌线程
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码
📌多线程相关概念
🥪什么是多线程
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术.
🌭线程与进程的关系
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
🥐Java的线程与操作系统线程的关系
- 线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
- Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.接下来我们就要多次用到这里提到的Thread类.
📌多线程编程实现方式
🧀继承Thread类
- 创建一个类
class MyThread extends Thread {
@Override
public void run() {//相当于 线程的入口方法 线程跑起来后要做什么都是run来描述的
super.run();
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();//创建线程.此操作会在底层调用操作系统提供的"创建线程"的API
//同时在操作系统内核中创建对应的PCB结构,并且加入到对应的链表中
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
- 基于匿名内部类
//通过匿名内部类,创建线程
public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
};
t.start();
}
}
此处相当于是创建了一个子类,这个子类继承自Thread,但这个子类是没有名字的,这也是它叫作’匿名’的原因
另外注意,此内部类的创建是在当前的类里创建的,并使用 t 指向了该子类的实例,同时在子类中重写了run方法
🌮实现Runnable接口
- 创建一个类
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo3 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
}
}
这种方法把要完成的工作放到Runnable中,再让Runnable与Thread结合.进一步的将线程本身和要执行的任务解耦合了
- 基于匿名内部类
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});//此处小括号代表Thread构造方法的结束
t.start();
}
}
此处相当于是创建了一个Runnable的子类(实现Runnable)
另外注意,该子类创建出实例后,把实例传给了Thread的构造方法.同时在子类中重写了run方法
🍱使用lambda表达式,表示run方法的内容(推荐使用)
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(()-> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
- 引申:回调函数
lambda表达式本质上是一个"匿名函数",主要就可用来作为回调函数使用,回调函数不需要程序员自己去调用,而是在合适的时机被自动调用。
常用到回调函数的场景:
1.服务器开发:服务器收到一个请求触发一个对应回调函数
2.图形界面开发:用户某个操作触发一个对应的回调函数
🍤终止线程
- 手动设置标志位
public class Demo8 {
public static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (! isQuit) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
//修改标志位
isQuit = true;
System.out.println("线程终止");
}
}
- 引申:变量捕获
有一个客观存在的事实:如果 isQuit 定义于main方法中,而lambda表达式执行时机是更靠后的,这就会导致后续开始执行lambda时,isQuit变量可能已经被销毁了。因此lambda中引入了“变量捕获”这个机制,把外部的变量复制了一份到lambda中,从而解决生命周期的问题
- 现成的标志位
Thread类提供了现成的标志位,可以不用手动设置
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//currentThread()是获取线程引用的静态方法,此处的指代的就是t
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
/*break;*/
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
但是在运行该代码后我们发现了一个问题,在线程执行的过程中报出了如下的一个异常,更奇怪的是,该线程还在继续输出内容。
这是因为一个线程,在一个时刻可能是在执行,也可能是在sleep,那如果在sleep过程中将线程中断,是否需要把它“唤醒”呢。
当然是需要的。因此如果线程在sleep过程中,其他线程调用了interrupt方法,就会强制使sleep抛出一个异常,sleep也就被立即唤醒。但是在被唤醒的同时,会自动清除前面的标志位。 循环也会继续执行。此时如果想让线程结束,直接在catch中加break即可。
此时我们发现线程就会在报出异常后中断。
🍛等待线程
多线程执行时是并发的,具体的执行过程由操作系统负责调度。因此无法确定线程执行的先后顺序,因此引入了等待线程的方法,也就是规划线程结束顺序的一种手段.用到了join()方法
public class Demo10 {
public static void main(String[] args) {
Thread a = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello a");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("a ending...");
});
Thread b = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello b");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("b ending...");
});
a.start();
b.start();
}
}
通过上述代码可以看出b的循环次数要少于a,所以b线程会早于a线程结束。但如果我想手动干预使a线程先结束呢?
那么只要在b线程中加入a.join()方法,此时,a线程还没有执行完,b线程就会进入’阻塞’状态,就相当于给a留下了执行的空间,a进程执行完后,b才会从阻塞状态中恢复,并继续往后执行(如果b执行到a.join()时,a已经执行完毕了那就不会进入到阻塞状态,直接往下执行)
代码如下:
public class Demo10 {
public static void main(String[] args) {
Thread a = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello a");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("a ending...");
});
Thread b = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello b");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//让b进入阻塞状态
try {
a.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("b ending...");
});
a.start();
b.start();
}
}
- 阻塞
怎么理解这个阻塞呢?我们可以认为阻塞即使该线程暂时不去CPU上执行调度。
因此sleep也可以实现线程的阻塞,但阻塞是有时间限制的。而上面用到的join()方法则是永久的等待。
那我们就要考虑到这种“死等”的方式是否合适呢?明显是不合适的,在计算机中时间力度很强,如果无休止的等待可能会造成资源的浪费,因此我们建议限制等待的最大时间。如下方法:
📌线程的状态
线程的状态是一个枚举类型,我们可以通过getState()方法来查看.
NEW | 安排了工作, 还未开始行动 |
RUNNABLE | 可工作的. 又可以分成正在工作中和即将开始工作 |
BLOCKED | 因为锁产生阻塞了 |
WAITING | 因为调用wait产生阻塞了 |
TIMED_WAITING | 因为sleep产生阻塞 |
TERMINATED | 工作完成了 |
我们通过一段代码来观察一下。
//线程的状态
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println(t.getState());//NEW
t.start();
System.out.println(t.getState());//RUNNABLE
t.join();
System.out.println(t.getState());//TERMINATED
}
}
三个获取状态的位置分别就代表安排了工作但未行动、正在工作与工作结束。
以下就是各状态间的相互转移与关系
🎉总结
🎇到这里本篇文章就结束了,感谢大家的阅读。非常希望大家可以提出宝贵的建议!!
🎇本篇文章是博主对自己学习中所学知识的个人理解。日后我也将不断更新自己学习过程以达到温故知新。
🎇希望大家可以点赞+收藏+评论支持一下噢!
🎇继续保持关注噢~