目录

 

1.JVM线程模型

1.1 主内存和工作区内存

1.2 线程安全

2.java线程的状态

2.1. 初始状态(NEW)

2.2.1就绪状态(RUNNABLE之READY)

2.2.2运行中状态(RUNNABLE之RUNNING)

2.3 阻塞状态(BLOCKED)

2.4 等待(WAITING)

2.5 超时等待(TIMED_WAITING)

2.6 终止状态(TERMINATED)

3.线程上下文切换

3.1资源开销

上下文切换

运行队列

利用率

1 、us

2、sy


1.JVM线程模型

借鉴:https://blog.csdn.net/u014730165/article/details/81981154

在了解JVM线程模型之前你需要充分了解JVM的内存模型

例如 堆:方法区是线程共有的 栈:是每个线程私有的

JVM本质上是操作系统的一种镜像(我的理解是在电脑上打开了一种特殊格式的软件,也就是存储设备上的,虽然同为操作系统,软件就不是直接使用处理器的资源来操作了,而是通过一些特殊指令来访问本机的操作系统,来调用相关的操作系统资源),是软件层面上的虚拟机。

在理解JVM线程模型之前需要先看看,物理计算器上用户磁盘和cpu的交互,由于cpu读写速度速度远远大于磁盘的读写速度速度,所以有了内存(高速缓存区)。但是随着cpu的发展,内存的读写也跟不上cpu的读写速度了,cpu的产商就给每个cpu加入了一个高速缓存,也就是下面的结构(之所以介绍这个是因为jvm线程模型和这个很相似)

这里写图片描述

1.1 主内存和工作区内存

  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。
  Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示

这里写图片描述

1.2 线程安全

通过上面分析,可以发现,每个线程都拥有自己的工作内存,工作内存是线程私有的。所以每个线程对堆中的共享变量进行修改对其他的线程而言是不可见的。
  Java内存模型中,程序(进程)拥有一块内存空间,可以被所有的线程共享,即MainMemory(主内存:堆);而每个线程又有一块独立的内存空间,即WorkingMemory(工作内存:栈)。普通情况下,当线程需要对某一共享变量进行修改时,通常会进行如下的过程:
1.从主内存中拷贝变量的一份副本,并装载到工作内存中;
2.在工作内存中执行代码,修改副本的值;
3.用工作内存中的副本值更新主存中的相关变量值。
  所谓“线程安全”,即多个线程同时执行同一段代码时,不会出现不确定的或者与单线程条件下不一致的结果。通常,下列三种条件居其一的并发访问被JVM认为是线程安全的:

有final关键字修饰且已被赋值;
有volatile关键字修饰;
有锁保护(synchronized、ReentrantLock等)。
 

2.java线程的状态

借鉴于https://blog.csdn.net/pange1991/article/details/53860651

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

2.1. 初始状态(NEW)


实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

 

2.2.1就绪状态(RUNNABLE之READY)


就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。


2.2.2运行中状态(RUNNABLE之RUNNING)


线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

2.3 阻塞状态(BLOCKED)


阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

2.4 等待(WAITING)


处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

2.5 超时等待(TIMED_WAITING)


处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

2.6 终止状态(TERMINATED)


当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程状态图

3.线程上下文切换

所谓线程切换,本质就是“数据在线程间切换”,即将一个线程A中的数据,传递到另一个线程B执行数据处理操作。基于以上认知,比较自然的实现逻就是:将线程A中的数据进行拷贝,线程B获取到拷贝数据,然后进行处理,如下图所示。

 

借鉴于:https://blog.csdn.net/weixin_42373066/article/details/114887981

3.1资源开销

在Linux中,CPU主要用于中断、内核以及用户进程的任务处理,优先级为中断>内核>用户进程,在学习如何分析CPU消耗状况前。先要掌握三个重要的概念

上下文切换

每个CPU在同一时间只能执行一个线程,Linux采用的是抢占式调度,即为每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或高级优先线程要执行时,Linux将切换执行线程,在切换时要存储目前线程的执行状态,并要恢复要执行的线程状态,这个过程称为上下文切换。对于java应用,典型的是进行文件IO操作、网络IO操作、锁等待或线程sleep时,当前线程会进入阻塞或休眠状态,从而促发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用的相应速度下降。

运行队列

 

每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,JAVA应用中启动了8个线程。且这8个线程都初一可运行的状态,那么在分配平均的情况下每个CPU中运行队列里就会有两个线程。通常而言,系统的load主要由cpu的运行队列来决定,假定以上状况运行维持了1分钟,那么这1分钟内系统load就会是2,但用于load是个复杂的值,因此也不是绝对的,运行队列值越大,就意味着线程会要消耗长时间才能执行完。Linux System and NetWork Performance Monitoring中建议控制在每个cpu核上的运行队列为1~3个。

利用率

 

CPU利用率为CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分使用百分比,这5个值用来分析CPU消耗情况的关键指标。Linux System and NetWork Performance Monitoring中建议用户的CPU消耗/内核的CPU消耗的比率在65%~70%/30%~35%左右。

使用top命令查看

top - 23:22:02 up  5:47,  1 user,  load average: 0.00, 0.00, 0.00

Tasks: 107 total,   1 running, 106 sleeping,   0 stopped,   0 zombie

Cpu(s):  0.3%us,  0.3%sy,  0.0%ni, 99.3%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st

Mem:   1017464k total,   359292k used,   658172k free,    56748k buffers

Swap:  2064376k total,        0k used,  2064376k free,   106200k cached

此信息关注第三行信息

0.4%us表示用户处理进程所占的百分比;0.3%sy表示内核线程处理所占的百分比;0.0%ni表示被nice命令改变优先级的任务所占的百分比;99.3%id表示CPU的空闲时间所占的百分比;0.0%wa表示为在执行过程中等待IO所占的百分比;0.0%hi表示硬件中断所占的百分比,0.0%si表示为软件中断所占的百分比。

对于多核或者对歌CPU,上面的显示或是多个cpu所占用的百分比的综合,因此会出现160%us这样的现象,如需要查看每个核的消耗情况,可在进入top视图后按1,就会按核来显示消耗情况

默认情况下top视图中显示的为进程的CPU消耗状况,在top视图中按shift+h后,可按线程查看CPU的消耗状况,

1313 root      20   0  165m 3848 3024 S  0.0  0.4   0:05.06 vmtoolsd

2767 xubo      20   0 14940 1176  904 R  0.3  0.1   0:02.93 top

1 root      20   0 19228 1428 1148 S  0.0  0.1   0:01.86 init

1949 root      20   0  150m  20m 4976 S  0.0  2.0   0:01.58 Xorg

2076 gdm       20   0  363m  14m  10m S  0.0  1.5   0:01.01 gdm-simple-gree

还可以使用pidstat  vmstat

pidstat是SYSSTAT中的工具,如须使用pidstat,请先安装SYSSTAT

当CPU消耗严重时,主要体现在us、sy、wa或hi的值变高,wa的值是IO等待造成的,hi的值变高主要是硬件中断造成的,例如网卡接收数据频繁的状况。

对于java应用而言,CPU消耗严重主要体现在us、sy两个值上,分别看看java应用在两个值高的情况下应如何寻找对应造成平静的 代码。

1 、us

 

当us值过高时,表示运行的应用消耗了大部分的CPU。在这宗情况下,对JAVA应用而言,最重要的为找到具体消耗CPU的线程所执行的线程执行代码,可采用如下犯法做到。

首先通过linux提供的命令找到消耗CPU严重的线程及其ID,将线程ID转化为十六进制的值。之后通过 kill -3 [javapid]或jstack的方式dump出应用的java线程信息,通过之前转化出的十六进制的值找到对应的nid值的线程。该线程即为消耗CPU的线程,在采样时候须执行几次上述的过程,确保找到真实消耗CPU的线程

java应用造成us高的原因主要是线程一直处于可运行状态,通常是这些线程在执行无阻塞、循环、正则或纯粹的计算等动作造成;另外一个可能是频繁GC。如每次请求都需要分配较多的内存,当访问量搞的时候就将导致不断地进行GC,系统响应速度下降,进而造成堆积的请求更多,消耗的内存更严重,最严重的时候有肯能会导致系统不断的惊醒Full GC,对于频繁GC的状况要通过分析JVM内存消耗来查找原因

具体事例:请参考分布式java应用180页

2、sy

 

当sy值高时,表示Linux花费了更多的时间在进行线程切换,Java应用造成这种现象的主要原因是启动的线程比较多,且这些线程多数处理不断的阻塞(如锁等待、IO等待状态)和执行状态的编号过程中,这就导致了操作系统要不断切换执行的线程,产生大量的上下文切换。在这种状况下,对Java应用,最重要的是找出线程不断切换状态的原因。
 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐