一、进程与线程
1、进程与线程
1.1、进程
程序一般认为是静态存储的,而进程则是活动的,真正执行的、动态加载到内存的,被CPU执行的。
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 程序是静态的,可以把进程看成是一个实例。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)
1.2、线程
- 一个进程之内可以分为一到多个线程
- ·一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU执行·
- Java 中,线程作为最小调度单位(调度单位:CPU根据线程去找对应的指令),进程作为资源分配的最小单位(进程是管资源,加载指令,管理内存,管理IO)。在 windows 中进程是不活动的,只是作为线程的容器 【线程是负责执行指令,进程是管理指令】
1.3、两者比较
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集进程拥有共享的资源,如内存空间(管理和分配内存空间)等,供其内部的线程共享
- 进程间通信较为复杂:
- 同一台计算机的进程通信称为IPC (Inter-process communication)·
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
上下文切换:举个简单例子,当电脑内存不够时,要执行一个任务,可以把暂时不用的代码临时存储起来,然后切换到其他进程执行。
2、并行和并发
2.1、并发
当要让CPU同一时间执行多个线程,需要依赖操作系统中的任务调度器,可以把CPU的时间(时间片)分给不同的线程使用(也就是让多个线程轮流的去使用CPU)。让CPU轮流去执行多个线程(当一个线程的指令执行一半时,下次又让CPU去执行,可以在上一次执行的结束位置开始继续去往下执行指令),这样同一时间内应对多件事情的能力,称为 “并发”的能力。
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的线程使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的
总结为一句话就是:微观串行,宏观并行般会将这种 线程轮流使用 CPU 的做法称为并发,concurrent
2.2、并行
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
即使是多核CPU,也不可能是完全并行执行线程,但分配给一个线程的时间片用完,还是会分配给其他线程,因此,这种情况下是并发+并行。
- 并发 (concurrent) 是同一时间应对 (dealing with) 多件事情的能力
- 并行 (parallel) 是同一时间动手做 (doing) 多件事情的能力
3、异步、同步调用
从方法调用的角度来讲,如果
- ·需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
注意:同步在多线程中还有另外一层意思,是让多个线程步调一致
1)多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒调用者什么都做不了,其代码都得暂停...
2)比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程ui程序中,开线程进行其他操作,避免阻塞ui 线程
二、java线程
1、创建和运行线程
每个java程序在运行的时候,都会 创建一个线程,该线程为主线程
①方法一:直接使用Thread
需要注意的是,在定义好java中的Thread对象,在还没启动前,并没有和CPU中的线程相关联。需要调用Thread中的start启动后,才真正的把该线程交给操作系统中的任务调度器,并将其时间片分配给CPU去执行。
Thread t1 = new Thread("t1"){
//线程要执行的任务(重写的run方法)
@Override
public void run() {
log.debug("执行任务1!");
}
};
t1.start();
log.debug("执行任务2!");
②方法二:使用Runnable配合Thread
把线程的任务分离出来
Runnable r = new Runnable() {
@Override
public void run() {
log.debug("执行任务1");
}
};
// Runnable r1 = ()-> log.debug("执行任务1");
//直接把runnable的lambda写法返回结果传到Thread构造方法中
Thread t1 = new Thread(()->{
//线程要执行的任务(重写的run方法)
//...
log.debug("执行任务2!");
});
Thread t = new Thread(r);
t.start();
t1.start();
③方法三:FutureTask能够接收Callable 类型的参数,用来处理有返回结果的情况
//创建FutureTask任务对象,此处注意FutureTask内部构造方法需要创建一个Callable实例,重写call方法返回结果
FutureTask<Integer>futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行任务");
Thread.sleep(2000);
return 200;
}
});
Thread t = new Thread(futureTask,"t1");
t.start();
//注意要返回task的结果,需要调用FutureTask的get方法,由于任务对象中调用Thread.sleep(2000),因此需要等待线程2s后才会返回200给主线程main
int result = futureTask.get();
log.debug("result="+result);
2、查看进程线程的方法
Windows
- tasklist 查看进程
- taskkill 杀死进程
Linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程
- kill 杀死进程
- top 按大写H切换是否显示进程
- top -H -p 查看某个进程(PID)
Java
- jps命令查看所有java进程
- jstack查看某个Java进程(PID)的所有线程状态
- jconsole来查看某个Java进程中线程的运行情况(图形界面)
jconsole远程监控配置:
【需要以如下方式运行你的java类】
java -Djava.rmi.server.hostname='ip地址-Dcom.sun.management.imxremote-Dcom.sun,management.jmxremote,port=~连接端口’-Dcom,sun,management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.imxremote.authenticate=是否认证 java类
3、线程的原理
3.1、栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存
每个栈由多个栈帧(Frame)组成,栈帧对应着栈中每次方法调用时所占用的内存。每个线程只能有一个活动栈,对应着当前正在执行的那个方法
3.2、栈帧图解
(1)类加载 (注意:方法区在本地内存)
(2)给main线程分配栈内存,并将main线程交给任务调度器调度执行,之后CPU就把时间片分配给主线程
程序计数器是栈私有的,记录当前该执行哪行代码了,CPU根据程序计数器的记录来知道要从哪个代码开始执行(可以理解为一个存档点)
返回地址对应程序的退出地址
3.3、多线程
示例演示:
@Slf4j
public class Test {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
fun1(10);
}
});
t.start();
fun1(20);
}
public static void fun1(Integer temp){
log.debug("fun1:"+temp);
fun2(temp);
}
public static void fun2(Integer temp){
log.debug("fun2:"+temp);
}
}
分别给主线程中的fun1(20)和t线程中的fun1(10)打断点,观察栈帧的变化
【注意】断点得选择Thread,选All就看不了两个线程分别运行,只能看到同时运行的结果
除了通过这里IDEA观察,也可以使用命令行jconsole观察
3.4、线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码(可以简单理解为,一个线程的使用,到不使用的过程,即为 “线程上下文切换”)
- 线程的cpu时间片用完
- 垃圾回收(会暂停当前所有的工作线程,让垃圾回收的线程去回收垃圾,这时工作线程就会全部暂停,暂停就会导致上下文切换)
- 有更高优先级的线程需要运行
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法当Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch频繁发生会影响性能(当CPU核心数少于线程数时,CPU就会频繁地在这些线程中轮流切换,那么线程上下文切换的成本也就越高,因此会影响性能)
4、常用方法
方法 | 功能说明 | 注意 |
start() | 启动一个新线程在新的线程运行run方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | 如果在构造Thread对象时传递了Runnable 参数,则线程启动后会调用Runnable中的run 方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为 |
join() | 等待线程运行结束 | |
join(long n) | 等待线程运行结束最多等待 n毫秒 | |
getId() | 获取线程长整型的id 唯一id | |
getName) | 获取线程名 | |
setName(String) | 修改线程名 | |
getPriority() | 获取线程优先级 | |
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的机率 |
getState() | 获取线程状态 | Java中线程状态是用6个enum 表示,分别为:NEW、RUNNABLE(就绪)、BLOCKED、RUNNING(运行)、WAITING、TIMED WAITING(阻塞) |
isInterrupted () | 判断线程是否被打断 | 不会清除打断标记 |
interrupt() | 打断线程 | 如果被打断线程正在sleep,wait,join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记 |
interrupted() static | 判断当前线程是否被打断 | 会清除打断标记 |
isAlive() | 线程是否存活(还没有运行完毕) | |
currentThread() static | 获取当前正在执行的线程 | |
sleep(long n) static | 让当前执行的线程休眠n毫秒,休眠时让出cpu 的时间片给其它线程 | |
yield() static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |