深入理解jvm线程和线程监控
1、我们的后端服务可以处理很高的QPS远远超过cpu的核数,但cpu占用还是很少,这是为什么呢?
2、后端服务经常会使用线程池,有的应用一个就会创建很多线程池,但实际上我们的cpu都是四核心的,根本不满足我们运行那么多线程的需求,那又是为什么可以这样用呢?
1、进程VS线程
1.1、概念介绍
进程(procss): 是操作系统为正在运行的程序提供的抽象,是操作系统对资源分配的基本单位,比如我们电脑上运行的各种程序,每次运行的web项目都是一个单独的进程。
**线程(thread)**线程是资源调度的基本单位,线程是属于某一个进程的,一个进程中可以有多个线程存在,它们共享这个进程的资源,但是每个线程都拥有自己的程序计数器,虚拟机栈,本地方法栈 。
1.2、为什么会有进程和线程?
我们日常使用电脑,就是运行各个程序为我们所使用,要想运行这些程序,就需要电脑资源进行支持,包括CPU,内存,网络,硬盘等等,但真正需要运行指令的是靠CPU进行计算的,CPU的速度非常快而且价格也非常昂贵,目前普遍都是2核或者4核的cpu,如果一个cpu只能运行一个程序的话,那我们每台电脑基本只能运行几个程序了,完全不能发挥出电脑的作用,为什么现在的电脑都可以同时打开很多软件进行正常的运行呢?
操作系统会拆分CPU为一段段时间的运行片,轮流分配给不同的程序。对于多cpu,多个进程可以并行在多个cpu中计算,当然也会存在进程切换;对于单cpu,多个进程在这个单cpu中是并发运行,根据时间片读取上下文+执行程序+保存上下文。同一个进程同一时间段只能在一个cpu中运行,如果进程数小于cpu数,那么未使用的cpu将会空闲。
多线程的概念主要有两种:一种是用户态多线程;一种是内核态多线程,对于内核态多线程(java1.2之后用内核级线程),在操作系统内核的支持下可以在多核下并行运行; 对于多核cpu,进程中的多线程并行执行。对于单核cpu,多线程在单cpu中并发执行,根据时间片切换线程。同一个线程同一时间段只能在一个cpu内核中运行,如果线程数小于cpu内核数,那么将有多余的内核空闲。
1.3、线程和进程的区别?
- 传统UNIX操作系统是以进程为最小单元进行管理运行的程序的,如果需要并行处理任务,需要创建多个进程来实现,当主进程接收到一些指令需要进行并行处理的时候,是通过fork()出一个子线程进行处理的,虽然这样也可以实现并行处理,但有两点主要的问题
- 进程间数据资源不能共享
- 进程创建和销毁需要消耗大量性能
- 线程是对进程的更小细分,可以说是轻量级的进程,创建和销毁线程不需要进行资源的分配,它共享进程的资源。在许多系统中,创建一个线程要比创建一个进程快 10 - 100 倍。同一个进程中可以创建多个线程,它们可以独立并发运行,并且共享进程中的内存数据。
2、JVM中的线程
2.1、如何创建线程
继承Thread进行线程创建
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeThread p = new PrimeThread(143);
p.start();
实现Runnable方法
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
Thread和Runnable 从JDK1.0就已经有了
2.2、源码分析
创建线程初始化
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
启动线程
public synchronized void start() {
/**
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then it will be passed up the call stack */
}
}
}
#原生C方法,最终会调用操作系统的方法
private native void start0();
native方法源码解析https://www.cnblogs.com/lusaisai/p/12729334.html
2.3、线程状态
2.3.1、JVM线程状态:
A thread state. A thread can be in one of the following states:
- NEW A thread that has not yet started is in this state.(线程刚创建时默认状态)
- RUNNABLE A thread executing in the Java virtual machine is in this state.(运行状态)
- BLOCKED A thread that is blocked waiting for a monitor lock is in this state.(阻塞状态)
- WAITING A thread that is waiting indefinitely for another thread to perform a particular action is in this state.(等待状态)
- TIMED_WAITING A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.(超时等待状态)
- TERMINATED A thread that has exited is in this state.(终止状态)
A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflect any operating system thread states.
Since: 1.5
See Also:getState
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
2.3.2、操作系统线程状态:
- NEW
- READY
- RUNNING
- BLOCKED
- TERMINATED
2.3.3、线程状态流转图:
2.3.4、操作系统状态和JVM状态对比与联系
操作系统 | JVM | 备注 |
---|---|---|
New | New | |
Ready | Running | 等待cpu片段调度运行 |
Running | 真正消耗CPU资源,进入运行状态 | |
Blocked | Blocked | Java中未获取到锁 |
Waiting | 调用了wait/join/park主动陷入阻塞 | |
TimedWaiting | 调用了sleep/wait/join/park等主动陷入阻塞,并设置了超时唤醒时间 | |
Terminated | Terminated |
3、线程监控
我们知道了如何创建线程和线程有哪些状态,那我们怎么去验证呢?当线程出现问题的时候怎么去排查呢?
3.1、使用工具进行线程监控
3.1.1、使用jvisualvm工具
jdk自带的监控软件,可以在jdk的安装目录里找到
监控本地jvm进程
打开软件后,在本地运行的所有jvm进程都会在软件左侧的菜单中进行自动展示
我们可以写一段需要CPU计算的代码进行模拟
public class Thread1Test {
public static void main(String[] args) {
DemoThread thread = new DemoThread();
thread.start();
}
static class DemoThread extends Thread {
public DemoThread() {
super("demoThread1");
}
@Override
public void run() {
while (true) {
runTask();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 计算圆周率
public void runTask() {
double p = 2;
for (int i = 0; i < 500000000; i++) {
p *= ((double) (((int) ((i + 2) / 2)) * 2)) / (((int) ((i + 1) / 2)) * 2 + 1);
}
System.out.println("Thread run result=" + p);
}
}
}
点击上方线程的菜单,可以看到当前jvm的所有线程及运行情况
点击线程dump可以看到线程当前的状态:
通过抽样器我们可以发现sleep是不消耗cpu资源的,真正消耗cpu资源的只有代码运行的方法
3.1.2、使用jstack命令
适合在无UI系统的服务器上使用,jdk自带命令
1、先使用jps命令找到需要查询的jvm进程pid
2、使用jstack命令
和上面的线程dump结果是一样的
3.2、线程其他状态验证Case
3.2.1、网络IO
目前后端主要的IO操作都在网络上,很少有磁盘IO,比如查询redis缓存,查询DB数据,查询ES数据,调用其他服务的接口等通过网络调用获取数据的形式
服务调用代码列子
public class ThreadTest2 {
public static final String URL = "http://localhost:8088/slowTime/sleepTime?time=5000";
public static void main(String[] args) {
DemoThread2 thread2 = new DemoThread2();
thread2.start();
}
static class DemoThread2 extends Thread {
public DemoThread2() {
super("DemoThread2");
}
@Override
public void run() {
while (true) {
runTask();
}
}
public void runTask() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(URL)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
String resStr = response.body().string();
System.out.println("res=" + resStr);
}
} catch (Exception e) {
System.err.println("调用服务失败:" + e.getMessage());
}
}
}
}
@RestController()
@RequestMapping("slowTime")
@Slf4j
public class SlowTimeController {
@GetMapping("sleepTime")
public String sleepTime(@RequestParam("time") Long time) {
if (time == null) {
time = 100L;
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
log.error("休眠异常", e);
}
return "SUCCESS";
}
}
线程状态监控结果:
可以发现网络IO操作一直是运行状态
我们再看下cpu使用情况:
几乎看不到有在使用过,是不是很奇怪,为什么线程在Running状态,但是cpu却没有任何资源的消耗呢?
Java将操作系统的IO阻塞状态对应到自己的****Running状态了,虽然此时是阻塞状态,但是是不消耗操作系统的CPU资源的,为什么Java需要这样处理呢?
当线程调用阻塞API时,线程进入休眠状态,这是指操作系统级别。从JVM层面来看,java线程状态还处于runnable状态。JVM 不关心操作系统线程的实际状态。从JVM的角度来看,等待CPU使用率(操作系统线程状态处于runnable状态)和等待I/O(操作系统线程状态处于睡眠状态)没有区别。它们都在等待某个资源,所以都处于可运行状态。
3.2.2、获取锁
代码示列:
public class ThreadDemoLockTest {
private static final Object LOCK = new Object();
public static void main(String[] args) {
DemoThread thread3 = new DemoThread("DemoThread3");
DemoThread thread4 = new DemoThread("DemoThread4");
thread3.start();
thread4.start();
}
static class DemoThread extends Thread {
public DemoThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
runTask();
System.out.println(Thread.currentThread().getName()+"运行完成");
}
}
public void runTask() {
synchronized (LOCK) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
}
}
线程状态监控:
从上面的分析来看,我们大部分后端服务都是IO密集型的而非CPU密集型,对CPU的依赖不是很高,很多时候线程都是处于IO BLOCK状态
3.3、线程问题分析Case
3.3.1、程序突然cpu负载过高且无法定位问题
我们的线上服务很可能出现这种情况,突然cpu使用率告警,cpu一直出现超过80%的占用的情况,但是看接口的请求量也没有大的调用量发生,这时候如果我们通过日志分析,调用分析等无法找到cpu使用率过高的问题,还能怎么去排查呢?
1、在服务器使用top 命令查看当前哪个进程长时间占用cpu的资源,这一步我们获取到进程的PID
top -c
2、根据pid获取进程中哪个线程最占用cpu资源
ps p 17403 -L -o pcpu,pmem,pid,tid,time,tname,cmd # 或者 top -H -p 17403
3、根据线程TID查询具体线程状态和堆栈信息
# printf "%x\n" 17441
// 使用与jvm运行相同的用户进行执行命令 jstack 17403 | grep '4421' -F
现在我们可以查询到出问题的线程是哪一个了,有线程名称,还有线程状态
4、继续借助jstack找到出问题的线程执行的代码
jstack 17403
备注:第2步骤中通过pid查询哪些线程占用高cpu的情况中,有时候可能出现多个线程均占用很高的cpu,这时候需要每个进行单独排查,大部分情况其他的线程为GC线程
4、使用线程池创建线程
虽然创建线程已经比创建进程快10-100倍,但是创建线程仍然需要调用操作系统方法,需要进行一些初始化分配,仍然会给cpu带来额外的开销
4.1、创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize | 核心线程数 |
---|---|
maxinumPoolSize | 最大线程数 |
keepAliveTime | 空闲线程存活时间 |
unit | 存活时间的单位 |
workQueue | 存放线程任务队列 |
threadFactory | 线程工厂,创建新线程 |
handler | 线程池拒绝处理后的任务 |
4.2、线程执行策略
4.3、线程池这些参数应该怎么调整呢?
4.3.1、区分是IO密集型任务还是cpu密集型任务
cpu密集型:
通过上面的分析可以发现对于cpu密集型的任务会长时间占用cpu的支援,对于多核系统,每个核心也只能执行一个任务,所以对于这类的资源,我们不应该在线程池中创建的线程总数远大于系统核心线程数,这由可能导致频繁的线程切换,效率更低,应该使用队列对任务进行暂存
Case cpu密集型任务:
每个任务预计处理时间 500ms 每秒预计提交100个任务 允许最大的等待时间为5s
根据上面的信息我们可以大致计算出以下几点
- 由于是cpu密集型,每个核数1s内只能执行2个任务
- 假设没有队列的情况,则需要cpu个数为 100 / (1000/500) = 50 个,但这个的效果是1S内全部完成任务
- 如果5S内需要处理完所有的任务,则需要 100 / (1000/500) / 5 = 10 个
- 这时候队列最大堆积为 100 - 10 = 90 个
由于cpu密集型,如果想发挥最大作用,只能提升机器性能,比如cpu升级为6核12线程的,设置 corePoolSize = maxinumPoolSize = 12;队列设置为100 就可以满足了
但如果cpu只有4核4线程呢?
- 每s最多处理8个任务
- 5s最多处理40个任务
- 不管我们怎么设置队列长度和调整线程数都是没有用的,最好的办法是提前拒绝任务
IO 密集型:
对于io密集型的任务,从上面的分析看大部分时间都不会消耗cpu资源的,cpu可以用空余的时间来处理更多的线程,来提高任务的吞吐率
Case io密集型任务
每个任务预计处理时间50ms,每秒预计提交1000个任务,允许最大等待时间为1s
根据上面的信息我们可以大致计算出以下几点
- 每个任务线程1s内可以处理 1000 / 50 = 20 个任务
- 1s内提交1000个任务,还要全部处理完毕,则实际需要线程数为 1000 / 20 = 50 个线程
- 根据线程执行策略,我们一次提交1000个任务,要么最大线程数要达到1000个,或者队列里可以暂存(1000 - 最大线程数),但从上条计算可知,实际上并不需要这么多线程进行处理,而且申请1000个线程对于操作系统来说也实属夸张,是肯定不可取的
假如系统cpu为4核4线程
我们使用如下设置进行测试:
可以发现如果我们设置的队列小了,是不能满足瞬时的流量峰值的,如果设置的过大,又不会创建新线程加快处理速度,这可怎么处理好呢?
推荐的参数配置参考为: