并发编程-进程和线程
进程和线程
进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
同⼀个进程内多个线程之间可以共享代码段、数据段、打开的⽂件等资源,但每个线程各⾃都有⼀套独⽴的寄存器和栈,这样可以确保线程的控制流是相对独⽴的。
对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并行与并发
单核 cpu 下,线程实际还是串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,
一般会将这种 线程轮流使用 CPU 的做法称为并发(concurrent)
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

JMH环境搭建
JMH,即Java Microbenchmark Harness,是专门用于代码微基准测试的工具套件,它会执行程序预热,执行多次测试并平均
简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化的分析。
JMH比较典型的应用场景有:
- 想准确的知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性;
- 对比接口不同实现在给定条件下的吞吐量,找到最优实现
- 查看多少百分比的请求在多长时间内完成
生成项目模版
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=com.javaming.study -DartifactId=springboot-concurrent -Dversion=0.0.1-SNAPSHOT
查看进程/线程信息
ps -ef | grep redis # 查看redis进程
ps -fT -p <PID> # 查看某个进程(PID)的所有线程

top # 按1 显示每个CPU信息 再按切换回统计后CPU信息

top -H -p <PID> # 查看某个进程(PID)的所有线程

线程运行原理

栈与栈帧
JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
线程常见方法
Sleep
- 调用 sleep 会让当前线程从
Running进入Timed Waiting状态(阻塞) - 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
yield
- 调用 yield 会让当前线程从
Running进入Runnable就绪状态,然后调度执行其它线程 - 具体的实现依赖于操作系统的任务调度器
join
主线程中加入t1.join()后就必须等待 t1执行完才能继续运行
如果t1.join(1000) 指定等待1秒,则1秒后 j1就会打印会=0
如果t1.join(3000) 指定等待3秒,则2秒后 j1就会打印会=10
static int j1 = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始运行");
Thread t1 = new Thread(() -> {
try {
sleep(2000);
j1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
//等待线程运行结束
t1.join(1000);
log.debug("结束运行 j1=" + j1);
}
interrupt
private static void testInterrupt() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
// 打断休眠的线程 会清空打断状态
log.error("被打断 isInterrupt = " + Thread.currentThread().isInterrupted());
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
// 打断正常运行的线程 不会清空打断状态
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.error("被打断 isInterrupt = " + Thread.currentThread().isInterrupted());
break;
}
}
}, "t2");
t2.start();
TimeUnit.SECONDS.sleep(1);
log.debug("interrupt...");
t1.interrupt();
t2.interrupt();
}
interrupted
和 interrupt 方法一样是判断当前线程是否被打断,但是这个方法是静态方法, 调用的时候会清除打断标记
查看打印结果可以知道:
- 打断休眠的线程 会清空打断状态
- 打断正常运行的线程 不会清空打断状态

两阶段终止模式
在一个线程t1中如何优雅的停止线程t2?
如果使用stop()方法: stop()方法会真正的杀死线程,如果线程锁住了共享资源,那么当他被杀死后就再也没有机会释放锁,其他线程永远无法获取锁
这个时候就依靠两阶段终止模式 来停止线程

public class TestStopThread {
private Thread monitorThread;
/**
* 开始监控线程
*/
public void start(){
monitorThread = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("处理善后工作");
break;
}
try {
TimeUnit.SECONDS.sleep(2);
log.debug("执行监控工作");
} catch (InterruptedException e) {
}
}
}, "monitor");
monitorThread.start();
}
/**
* 停止监控线程
*/
public void stop(){
monitorThread.interrupt();
}
}
interrupt 对 LockSupport.park()影响
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("park....");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态 = " + Thread.currentThread().isInterrupted());
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
可以看到即使重新LockSupport.park()后 也无法生效 ,因为park只有 打断标记为false时才能继续生效
interrupt 打断park的锁时 打断标记置为true , 即使重新park 也不会生效

// log.debug("打断状态 = " + Thread.currentThread().isInterrupted());
log.debug("打断状态 = " + Thread.interrupted());
修改为上面的语句后就可以正常锁住线程
守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
注意
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 和 Poller线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

浙公网安备 33010602011771号