进程和线程的比较
目录
一、前言
二、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);}@Overridepublic 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();}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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 {@Overridepublic 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{@Overridepublic 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使用率高的问题。后续将专门针对多线程开发进行介绍。