Java 多线程基础
1、并发编程中常见概念
1.1 进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,此时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
进程与线程的对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂。同一台计算机的进程通信称为 IPC(Inter-process communication)。不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.2 并行与并发
并发和并行都是 “多个线程/进程同时执行” 的意思,但实际上它们是有区别的。
并发和并行的区别:
- 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
并发(Concurrency)
早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。
所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。
高并发?
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
- 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
- 吞吐量:单位时间内处理的请求数量。
- QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
- 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
并行(Parallelism)
- 并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。
- 多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。
双核 CPU 执行两个任务时,每个核心各自执行一个任务,和单核 CPU 在两个任务之间不断切换相比,它的执行效率更高。
并发 + 并行
在图2中,执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。
例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:
每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。
总结
- 并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力。
- 单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。
- 在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段。
1.3 同步与异步
同步和异步关注的是消息通信机制 。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
1.4 阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。
2、Java 创建和运行线程
2.1 继承 Thread 类
/**
* @author : lyj
* @Timer : 2022/9/8
* @Description :
*/
public class TestThread {
public static void main(String[] args) {
System.out.println("main ..... start");
Thread01 thread01 = new Thread01();
thread01.start();
System.out.println("main ..... end");
}
public static class Thread01 extends Thread {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2 ;
System.out.println("运行结果:" + i);
}
}
}
从运行结果可以看出,main线程并不会等待thread01线程运行再运行(非阻塞)。
2.2 实现 Runnable 接口
把【线程】和【任务】(要执行的代码)分开。
-
Thread 代表线程
-
Runnable 可运行的任务(线程要执行的代码)
/**
* @author : lyj
* @Timer : 2022/9/8
* @Description :
*/
public class TestThread {
public static void main(String[] args) {
System.out.println("main ..... start");
Thread02 thread02 = new Thread02();
// 参数1 是任务对象; 参数2 是线程名字
Thread t2 = new Thread(thread02, "t2");
t2.start();
System.out.println("main ..... end");
}
public static class Thread02 implements Runnable {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}
}
}
2.3 实现 Callable 接口
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
/**
* @author : lyj
* @Timer : 2022/9/8
* @Description :
*/
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main ..... start");
FutureTask<Integer> futureTask = new FutureTask<>(new Thread03());
new Thread(futureTask).start();
// 阻塞等待线程执行完成,获得结果
Integer result = futureTask.get();
System.out.println("main ..... end.. " + " result: " + result);
}
public static class Thread03 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}
}
}
可以看出到futureTask.get();
这一段程序时,main线程 阻塞等待线程执行完成,获得结果、
3、查看进程线程的方法
Windows
# 查看指定端口的占用情况
netstat -aon|findstr "8080"
# 强制杀死指定端口
taskkill /pid <pid> -t -f
# 查看进程
tasklist
# 杀死进程
taskkill
Linux
# 查看所有进程
ps -fe
# 查看某个进程(PID)的所有线程
ps -fT -p <PID>
# 杀死进程
kill
# 按大写 H 切换是否显示线程
top
# 查看某个进程(PID)的所有线程
top -H -p <PID>
Java
# 命令查看所有 Java 进程
jps
# 查看某个 Java 进程(PID)的所有线程状态
jstack <PID>
# 使用Java自带的图形界面查看某个 Java 进程中线程的运行情况
jconsole
4、Thread 常见方法
4.1 run 和 start
4.2 sleep 和 yield
4.3 join 方法
4.4 interrupt 方法
5、线程状态
5.1 操作系统五状态模型
这是从 操作系统 层面来描述的
五状态模型图:
- 运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。
- 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
- 等待(wait)态:又称阻塞态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
- 新建态:对应于进程被创建时的状态,尚未进入就绪队列。创建一个进程需要通过两个步骤:① 为新进程分配所需要的资源和建立必要的管理信息。② 设置该进程为就绪态,并等待被调度执行。
- 终止态:指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。处于终止态的进程不再被调度执行,下一步将被系统撤销,最终从系统中消失。终止一个进程需要两个步骤:① 先对操作系统或相关的进程进行善后处理(如抽取信息)。② 回收占用的资源并被系统删除。
5.2 Java 六状态模型
根据 Thread.State 枚举,分为六种状态
public enum State {
// 尚未启动的线程的线程状态
NEW,
// 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
RUNNABLE,
// 等待监视器锁而阻塞的线程的线程状态。处于阻塞状态的线程正在等待监视锁进入同步块/方法,或者在调用Object.wait后重新进入同步块/方法。
BLOCKED,
// 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态: 对象。不超时等待 线程。没有超时的加入 LockSupport.park 处于等待状态的线程正在等待另一个线程执行特定的操作。例如,在对象上调用object.wait()的线程正在等待另一个线程在该对象上调用object.notify()或object.notifyall()。调用thread.join()的线程正在等待指定的线程终止。
WAITING,
// 具有指定等待时间的等待线程的线程状态。线程处于定时等待状态的原因是调用了以下方法中的一个并指定了正等待时间: thread.sleep 对象。等待与超时 线程。加入超时 LockSupport.parkNanos LockSupport.parkUntil
TIMED_WAITING,
// 已终止线程的线程状态。线程已完成执行。
TERMINATED;
}
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
参考:
评论区