1 基础概念
1 进程和线程
- 进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。
在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
- 每个进程都有自己独立的一块内存空间
线程
- 线程负责进程各种任务的执行
- 一个进程可以分为一个或多个线程,每个线程并发执行不同的任务
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。 在windows中进程是不活动的,只是作为线程的容器
进程和线程的区别
线程具有许多传统进程所具有的特征,故又称为轻型进程或进程元
根本区别
- 进程是操作系统资源分配的基本单位
- 线程是处理器任务调度和执行的基本单位
资源开销
- 每个进程都有独立的代码和程序上下文,进程之间的切换会有较大的开销;
- 线程可以看做轻量级的进程,每个线程都有自己独立的运行栈和程序计数器,因此线程之间切换的开销小
内存分配
- 同一进程的线程共享本进程的地址空间和资源
- 进程之间的地址空间和资源是相互独立的
崩溃的影响
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响
- 一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮
通信
- 进程间通信较为复杂,同一台计算机的进程通信称为IPC(Inter-process communication);不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
2 并行、并发与串行
串行
- 线程串行的情况下,n个任务由一个线程顺序执行。
- 由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
并发(concurrent)
- 同一时间段,多个任务交替执行 (单位时间内不一定同时执行);
- 单核CPU下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
- 总结为一句话就是:微观串行,宏观并行 ,
并行
- 单位时间内,多个任务同时执行
- 真正意义上的“同时进行”,真正的并行只能出现在拥有多核CPU的系统中,每个核都可以调度运行线程
- 即使是多核CPU,也会存在并发,因为线程数总是比CPU核多,因此实际生活中是并行与并发都存在
3 异步与同步
异步与同步是从方法调用的角度来看的
同步
需要等待结果返回,才能继续运行就是同步
- 异步
不需要等待结果返回,就能继续运行就是异步
4 多线程与并发编程
- 多线程概述
- 多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术
- 多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。
但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。
- 多线程的作用
- 单核CPU下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用CPU ,不至于一个线程总占用CPU,别的线程没法干活
- 多核CPU可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任
务都能拆分(参考阿姆达尔定律) - 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任
- IO操作不占用CPU,只是我们一般拷贝文件使用的是阻塞 IO,这时相当于线程虽然不用CPU,但需要一
直等待IO结束,没能充分利用线程。所以才有非阻塞 IO和异步 IO优化 - 多线程可以让方法执行变为异步的,而不是同步的,即不让方法干巴巴等着
比如说读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒cpu什么都做不了,其它代码都得暂停
- 并发编程概述
- 并发编程又叫多线程编程
- 使用并发编程的原因
多核的CPU的背景下,催生了并发编程的趋势
- 通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
方便进行业务拆分,提升系统并发能力和性能
- 多线程并发编程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
- 对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度
- 并发编程的缺点
并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题
A. 频繁的上下文切换
- 多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为时间片轮转,当一个线程分配的时间片用完时,就会重新处于就绪状态并将CPU核心让给其他线程使用,这个过程就属于一次上下文切换。
- 每次上下文切换时,需要保存当前的状态,以便能够进行恢复先前的状态,然后装载下一任务的状态,非常损耗性能,因此过于频繁的上下文切换反而无法发挥出多线程编程的优势。
- 由于上下文切换是个相对比较耗时的操作,所以有时并发未必会比串型速度快。相对而言,若是存在耗时任务放入线程中实际执行,线程使用成本可以不计。
B. 线程安全(死锁)
- 多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。
C. 内存泄漏
- 内存泄漏也称作”存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。
2 线程的状态
- 线程的状态可以从两个不同层面来看
- 从操作系统层面来看线程有5种状态
- 从Java API层面来看线程有6种状态
1 操作系统层面
- 初始状态
仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态(就绪状态)
指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 运行状态
指获取了CPU时间片运行中的状态
当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】。等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 终止状态
表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2 Java API层面
线程的生命周期
- 线程是一个动态执行的过程,它有一个从产生到死亡的过程
- 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
线程的状态
线程在运行的生命周期中的某时刻只可能处于下面6种不同状态中的一种
状态名 | 说明 |
---|---|
NEW 初始状态 |
线程被创建,但是还没有调用start()方法 |
RUNNABLE 运行状态 |
Java线程将操作系统中的就绪和运行两种状态统称为运行状态 实际上也涵盖了操作系统中的阻塞状态,因为IO导致的线程阻塞,在Java里无法区分,仍然认为是运行状态 |
BLOCKED 阻塞状态 |
表示线程阻塞于锁 |
WAITING 等待状态 |
表示当前线程需要等待其他线程做出一些特定动作,如通知或中断 |
TIME_WAITING 超时等待状态 |
该状态不同于WAITING,除了等待其他线程的一些特定动作,还可以在指定的时间到达后自行返回 |
TERMINATED 终止状态 |
表示当前线程已经执行完毕 |
- 常常把WAITING、TIME_WAITING、BLOCKED统称为线程阻塞,此时线程都不占用CPU
- 线程状态切换
- Thread或其子类 → NEW状态
- 使用
**new**
实例化线程后,就获得了一个线程对象,此时线程对象是NEW状态。
- NEW状态 → RUNNABLE状态
- 线程对象调用
**start()**
方法后将进入RUNNABLE状态 - 在RUNNABLE状态中,线程首先进入READY状态,在线程获得了CPU时间片后就处于RUNNING状态,此时将执行线程对象的
**run()**
方法。 - 操作系统隐藏了READY和RUNNING状态的切换,因此将这两个状态统称为RUNNABLE状态
- RUNNABLE状态 ↔ WAITING状态
- 当线程对象调用
**sleep()**
等方法后,线程将进入WAITING状态。 - 进入WAITING状态的线程需要依靠其他线程的特定动作(如通知或中断)才能够返回到RUNNABLE状态
- RUNNABLE状态 ↔ TIMED_WAITING状态
- TIMED_WAITING状态相当于在WAITING状态的基础上增加了超时限制,即调用方法时传入一个时间参数,如
wait(long millis)
或sleep(long millis)
- TIMED_WAITING状态和WAITING状态一样,可以通过其他线程的特定动作返回RUNNABLE状态,不同的是,当等待时间到达后,也会自动返回RUNNABLE状态
- RUNNABLE状态 ↔ BLOCKED状态
- 线程调用同步方法时,在获取
synchronized
同步锁失败后(因为同步锁被其他线程占用),线程将进入BLOCKED状态。 - 获取到同步锁后,线程将自动返回RUNNABLE状态
- RUNNABLE状态 → TERMINATED状态
- 线程在
**run()**
方法执行完成后,将进入TERMINATED状态。
3 Java线程
1 线程的创建与启动
1 创建线程
Java提供了两种最常见的创建线程的方法
- 继承Thread类
- 实现Runnable接口
- 线程池的相关方法也可以创建线程
Thread类
- Thread类是java.lang包下的一个类,实现了Runnable接口
- Thread类声明了一个线程必须拥有的方法(
run()
和start()
)以及一些线程常用方法 - 每一个Thread类和其子类的实例就代表一个处于某种状态的线程,因此创建线程就绕不开Thread类
- 无论采用什么创建线程的方法,最终创建的都是Thread实例
run()方法
- run()方法中定义了线程具体要执行的任务,无论采用什么方式创建线程,都必须重写/实现run()方法
- Thread类定义的run()方法几乎没有实现任何功能,因此必须重写run()方法,否则创建的线程就没有什么意义了。Thread定义的run()方法如下
// Thread类定义的run()方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
Runnable接口
Runnable是一个只声明了
run()
方法的接口@FunctionalInterface
public interface Runnable {
public abstract void run();
}
官方文档这样描述Runnable接口“Runnable接口应该被一个要通过线程执行其实例的类所实现”
- Thread类实现了Runnable接口,但是其重写的
run()
方法没有实际功能。 - 一个类只是实现Runnable接口,并不能代表一个线程,即Runnable接口和线程是两个不同的概念。
1 通过继承Thread类来创建线程
创建线程的步骤
- 定义一个继承Thread类的子类
- 重写Thread类的
run()
方法
实例 ```java public class MyClass { public static void main(String args[]) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.setName("子线程");
myThread.start();
System.out.println(Thread.currentThread().getName() + ":main()方法执行结束");
} }
class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + “:run()方法执行结束”); } }
上述代码输出
子线程:run()方法执行结束 main:main()方法执行结束
<a name="DB17A"></a>
#### 2 通过实现Runnable接口来创建线程
- **实现Runnable接口创建线程的优点**
- 实现Runnable接口来创建线程可以更容易与线程池等高级API配合
- 大多数情况下,**如果只想重写run()方法,而不重写Thread的其他方法,那么应使用Runnable接口**。因为**除非程序员打算修改或增强某类的基本行为,否则不应为该类创建子类**。
- **创建线程的步骤**
1. 定义Runnable接口实现类,并实现`run()`方法
1. **以Runnable接口实现类的实例作为参数传给Thead构造器,构造器构造的Thread才是真正的线程实例**
- 上述两步可以通过lambda表达式更为简洁的完成
- **相关源码**
```java
public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
private Thread(Runnable target, ...) {
this.target = target;
...
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
实例 ```java public class MyClass { public static void main(String args[]) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
Thread myThread = new Thread(myRunnable);
myThread.setName("子线程1");
//使用内部类
Thread myThread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":run()方法执行结束");
}
});
myThread2.setName("子线程2");
myThread.start();
myThread2.start();
System.out.println(Thread.currentThread().getName() + ":main()方法执行结束");
} }
class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + “:run()方法执行结束”); } }
上述代码输出
子线程1:run()方法执行结束 main:main()方法执行结束 子线程2:run()方法执行结束
<a name="FBBRc"></a>
### 2 启动线程
- **start()方法**
- `start()`方法是Thread类的一个实例方法,调用该方法将启动一个新的线程执行Thread实例内的run()方法
- 在创建线程时,都需要重写`run()`方法,然而`**run()**`**方法是不能手动调用的**,应该在线程获得CPU时间片后由系统调用。
- **如果手动调用run()方法,则执行run()方法的并不是一个新线程,而是当前线程**
- **start()方法的限制**
- **一个线程实例只能开启一个(一次)线程,一个线程实例一旦调用**`**start()**`**方法,不管线程是正常结束还是异常结束,都无法再次通过调用该实例的**`**start()**`**方法开启新的线程**
- 重复调用`start()`方法会抛出`IllegalThreadStateException`异常。
- **主线程**
- **当Java程序执行main方法的时候,就是在执行一个名叫main的线程,也称为主线程**。
在main方法(主线程)执行时,可以开启多个线程,多个线程并发执行,相互抢夺CPU
- **如果一个Java程序没有定义线程的话,所有方法都在主线程中执行。**
<a name="vDena"></a>
## 2 线程优先级
- 每一个Java线程都有一个优先级,这样**有助于操作系统确定线程的系统调度顺序**。
- Java线程的优先级是一个整数,其取值范围是`**Thread.MIN_PRIORITY**`**=1**到`**Thread.MAX_PRIORITY**`**=10**。
- 默认情况下,每一个线程都会分配一个优先级`**Thread.NORM_PRIORIT**`**=5**。
<br />
- **优先级的不确定性**
- 具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源**,**但是**线程优先级不能保证线程执行的顺序。** 如果CPU比较忙,那么优先级高的线程会 获得更多的时间片,但CPU闲时,优先级几乎没作用
- **一般情况下,不会对线程设定优先级别**,更**不会让某些业务严重地依赖线程的优先级别(例如任务的权重,借助优先级设定某个任务的权重是不可取的)**,一般定义线程的时候直接使用默认的优先级。
<a name="dsYe7"></a>
## 3 线程运行的原理
<a name="cRu2Z"></a>
### 1 栈与栈帧
- **回顾JVM相关知识**
![](https://cdn.nlark.com/yuque/0/2021/png/1169704/1622622643138-8f1d546f-28b5-40df-8574-e6fe57c77b6e.png#crop=0&crop=0&crop=1&crop=1&from=url&id=jPmjx&originHeight=855&originWidth=473&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- **栈**
- Java内存区域由堆、栈、方法区组成,栈存储了Java程序运行时信息,而且每个线程都有自己的虚拟机栈
- 每个栈由多个栈帧组成,栈帧存储了方法的**局部变量表、操作数栈、动态连接**和**方法的出口**等信息。
- **栈帧**
- 栈帧是用于**支持JVM进行方法调用和方法执行的数据结构**。**每个方法被执行的时候,JVM都会同步创建一个栈帧,当有一个方法被调用时,代表这个方法的栈帧入栈;当这个方法返回时,其栈帧出栈**。
- 每一个方法被调用到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
- 在活动线程中,**只有位于栈顶的栈帧才是有效的**,称为**当前栈帧**,与这个栈帧相关联的方法称为**当前方法**。**JVM运行的所有字节码指令都只针对当前栈帧进行操作**。
- **线程运行时的栈与栈帧**
- **代码**
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1169704/1629963927576-e7add417-de5f-46ab-b65e-d77e191fc55f.png#clientId=ub6513958-2aa4-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u72972be7&name=image.png&originHeight=311&originWidth=285&originalType=binary&ratio=1&rotation=0&showTitle=false&size=61532&status=done&style=none&taskId=u84d71db1-b8c4-41f6-942e-0d6b02cf02e&title=)
- **对应的Java内存区域**
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1169704/1629963975887-bd8cc45e-c03d-4e6a-bd4d-d49cb5ad904e.png#clientId=ub6513958-2aa4-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u699ff6d3&name=image.png&originHeight=465&originWidth=841&originalType=binary&ratio=1&rotation=0&showTitle=false&size=374242&status=done&style=none&taskId=u13b8dc2e-255e-4a52-bda1-cc2e3879f3c&title=)
<a name="HqqsJ"></a>
### 2 线程上下文切换
- **导致线程切换的原因<br />总的来说是线程放弃当前CPU使用权,交给另外一个线程使用**
**被动**
- 线程的CPU时间片用完
- 垃圾回收(垃圾回收会暂停所有工作线程,运行垃圾回收线程)
- 有更高优先级的线程需要运行
**主动**
- 线程自己调用了`Thread.sleep()`、`Thread.yield()`、`Object.wait()`、`thread.join()`、`LockSupport.park()`、`synchronized()`、`lock()`等方法
- **线程切换时发生的事情**
- 当线程切换发生时,需要由操作系统**保存当前线程的状态**,并恢复另一个线程的**状态**
- 线程的状态包括
- **线程的程序计数器**
- **虚拟机栈中每个栈帧的信息**
- 线程切换频繁发生会影响性能
<a name="Nk1fq"></a>
## 4 守护线程
- **Java中的线程分为两种**
1. **用户(User)线程**
**运行在前台**,执行具体的任务,如程序的main()线程、连接网络的子线程等都是用户线程
2. **守护(Daemon)线程**
**运行在后台**,专门服务于用户进程,**如垃圾回收线程**
- main()函数所在线程就是一个用户线程,**main()函数启动的同时JVM内部还启动了很多守护线程**
- **守护线程的特性**
- 由于守护线程时专门用于服务用户线程的,因此**如果用户线程都执行完毕,JVM只剩下守护线程的时候,未执行完的守护线程会随着JVM退出而被强制退出,不会再执行后续的代码**。
- **在守护线程中产生的新线程也是守护线程**
- **不是所有的任务都可以分配给守护线程来执行**,比如读写操作或者计算逻辑
- **守护线程内不能依靠finally块来确保执行关闭或清理资源的任务**
因为一旦所有用户线程都结束运行,守护线程就会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。
- 可以使用`thread.setDaemon(boolean on)`方法将线程thread设置为守护线程
设置守护线程时必须在thread调用`start()`方法前,否则会抛出`IllegalThreadStateException`异常
- **实例**
```java
public class MyClass {
public static void main(String[] args) {
Thread daemonThread = new daemonThread();
daemonThread.setDaemon(true);
daemonThread.start();
try {
Thread.sleep(800); //确保主线程结束前daemonThread能够分到时间片
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class daemonThread extends Thread {
@Override
public void run() {
while (true) {
try {
System.out.println("Demon Thread: I'm alive!");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Demon Thread: finally block");
}
}
}
}
上述代码输出
Demon Thread: I'm alive!
Demon Thread: finally block
Demon Thread: I'm alive!
- daemodThread的
run()
方法是一个死循环,但是当主线程结束后daemonThread就会退出,所以不会出现死循环的情况。 - 可以看到守护线程的finally语句块并不能保证执行
4 并发编程的要点
对于Java并发编程,有以下要点
- 线程安全性
- 线程的活跃性
- 性能
线程的安全性问题是首要解决的问题
线程不安全,运行出来的结果和预期不一致,那就连基本要求都没达到了