目录
一、前言
二、Linux查看进程、线程
2.1 Linux最大进程数
2.2 Linux最大线程数
2.3 Linux下CPU利用率高的排查
三、线程的实现
四、上下文切换
五、总结
一、前言
进程是程序执行相关资源(CPU、内存、磁盘等)分配的最小单元,是一系列线程的集合,进程之间相互独立,有自己的内存空间;线程是CPU资源分配的最小单元,线程需要的资源更少,可以看做是一种轻量级的进程,线程会共享进程中的内存,但线程使用独立的栈、程序计数器,线程相互通信更加方便。
在项目开发中,经常会用到线程以及多线程功能来实现异步任务处理等。项目上线之后,如果出现服务CPU高的异常情况,那么这个时候就需要借助Linux(因为一般情况服务都是使用Linux)查看进程、线程来定位最终的问题。
二、Linux查看进程、线程
2.1 Linux最大进程数
Linux中进程可创建的实际值通过进程标识值(process identification value)-PID来标示,可以使用
cat /proc/sys/kernel/pid_max 查看系统中可以创建的进程数实际值
可以使用ulimit命令修改最大限制值,
ulimit -u 1024
如果要修改kernel.pid_max的值,需要使用
sysctl -w kernel.pid_max=1024
2.2 Linux最大线程数
用ulimit -s可以查看默认的线程栈大小,一般情况下,这个值是8M=8192KB
不过Java程序受JVM堆空间的限制,比如以下代码
public class ThreadExample extends Thread{
public static void main(String[] args) {
for(int i = 0; i < 100000; i++){
ThreadExample myThread = new ThreadExample(i);
myThread.start();
}
}
private Integer threadNo;
ThreadExample(Integer threadNo){
threadNo = threadNo;
System.out.println("ThreadNo = " + threadNo);
}
@Override
public void run(){
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在idea上设置-Xmx1m,启动运行程序,创建出部分线程后,会报OutOfMemoryError错误
2.3 Linux下CPU使用率高的排查
示例代码如下,这段代码可以明显判断出来在while(true){count++;}的地方,会占用很高的CPU使用率,那么如果代码已经上线了,在生产上,我们如何来判断哪里出问题了呢?
public class CpuRatioExample extends Thread{
private Integer count = 0;
public static void main(String[] args) {
CpuRatioExample cpuRatioExample = new CpuRatioExample();
cpuRatioExample.start();
}
@Override
public void run(){
while(true){
count++;
}
}
}
第一步:运行编译后的class
java CpuRatioExample
程序运行之后,我们发现CPU使用率过高,这个时候,我们需要排查是哪个代码导致的,一般情况生产系统上都会做CPU、磁盘等基础设施的监控。
第二步:CPU使用率过高排查
top 命令查看哪个进程CPU使用率高
使用top命令发现 PID 1822的CPU占用异常,再进一步查找哪个线程导致的,
top -H -p pid 可以查看哪个线程cpu过高
第三步:使用jstack命令保存栈信息
jstack 1822 > 1822.stack
并分析栈信息,查找 1878线程对应的栈信息
stack信息是以16进制显示的, 所以需要将CPU使用率高的线程1878转换为十六进制 756;定位到在CpuRationExample的17行代码运行,结合源代码,定位了最终问题。
三、线程的实现
3.1 单线程的实现方式
3.1.1 Thread
public class ThreadExample extends Thread{
public static void main(String[] args) {
for(int i = 0; i < 100000; i++){
ThreadExample myThread = new ThreadExample(i);
myThread.start();
}
}
private Integer threadNo;
ThreadExample(Integer threadNo){
threadNo = threadNo;
System.out.println("ThreadNo = " + threadNo);
}
@Override
public void run(){
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这种方式很简单,但是不支持多重继承,所以不能继承其他类。
3.1.2 Runnable
public class ThreadExample implements Runnable{
public static void main(String[] args) {
for(int i = 0; i < 100000; i++){
ThreadExample myThread = new ThreadExample(i);
new Thread(myThread).start();
}
}
private Integer threadNo;
ThreadExample(Integer threadNo){
threadNo = threadNo;
System.out.println("ThreadNo = " + threadNo);
}
@Override
public void run(){
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这种方式比继承Thread类更灵活,因为一个类可以实现多个接口。
3.1.3 FetureTask
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadExample {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
// 异步执行的任务
return 1;
});
new Thread(futureTask).start();
try {
// 获取异步执行的结果
int result = futureTask.get();
System.out.println("result = " + result);
} catch (InterruptedException | ExecutionException e) {
// 处理异常
}
}
}
3.2 线程池的实现方式
有关线程池的,后续再详细介绍。
3.2.1 使用Executors类创建线程池
Executors.newFixedThreadPool(int nThreads):创建一个固定大小的线程池。
Executors.newCachedThreadPool():创建一个可以缓存线程的线程池。
Executors.newSingleThreadExecutor():创建一个单线程化的线程池。
3.2.2 使用ThreadPoolExecutor类创建线程池
ThreadPoolExecutor是一个更底层的类,允许开发者更精细地控制线程池的行为,比如:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<Runnable>() // workQueue
);
四、上下文切换
多线程和单线程的选择往往取决于具体的应用场景和需求,单线程是一次只做一件事,按照顺序执行,而多线程可以同时处理多个任务,抢占更多的系统资源,但是也会出现上下文切换,有些时候,多线程的性能未必比单线程要好。比如以下这段代码
public class DemoApplication {
public static void main(String[] args) {
//运行多线程
MultiThreadTester test1 = new MultiThreadTester();
test1.Start();
//运行单线程
SerialTester test2 = new SerialTester();
test2.Start();
}
static class MultiThreadTester extends ThreadContextSwitchTester {
@Override
public void Start() {
long start = System.currentTimeMillis();
MyRunnable myRunnable1 = new MyRunnable();
Thread[] threads = new Thread[3];
//创建多个线程
for (int i = 0; i < 3; i++) {
threads[i] = new Thread(myRunnable1);
threads[i].start();
}
for (int i = 0; i < 3; i++) {
try {
//等待一起运行完
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("multi thread exce time: " + (end - start) + "ms");
System.out.println("counter: " + counter);
}
// 创建一个实现Runnable的类
class MyRunnable implements Runnable {
public void run() {
while (counter < 100000000) {
synchronized (this) {
if(counter < 100000000) {
increaseCounter();
}
}
}
}
}
}
//创建一个单线程
static class SerialTester extends ThreadContextSwitchTester{
@Override
public void Start() {
long start = System.currentTimeMillis();
for (long i = 0; i < count; i++) {
increaseCounter();
}
long end = System.currentTimeMillis();
System.out.println("serial exec time: " + (end - start) + "ms");
System.out.println("counter: " + counter);
}
}
//父类
static abstract class ThreadContextSwitchTester {
public static final int count = 100000000;
public volatile int counter = 0;
public int getCount() {
return this.counter;
}
public void increaseCounter() {
this.counter += 1;
}
public abstract void Start();
}
}
这段代码的测试结果是,单线程的性能高于多线程的性能,其主要原因就是多线程的上下文切换导致性能降低。
如果想要进一步分析上下文切换情况,可以使用vmstat和pidstat分析上下文切换情况。
五、总结
本文介绍了进程和线程的区别以及Java如何开发单线程、多线程;linux下最大进程数、线程数的限制,以及如何通过jstack排查CPU使用率高的问题。后续将专门针对多线程开发进行介绍。