本文不从语言角度谈论好与不好。本文从性能测试角度分析一下Java线程与Golang协程的区别
用例设计
用 java
实现多线程任务处理:启动一定数量的等待线程或空转线程,并让启动的线程维持固定时间(60秒)
用golang
实现多协程任务处理:启动一定数量的等待协程或空转协程,并让启动的协程维持固定时间(60秒)
测试结果
Java
Golang
结果分析
内存使用
Java线程的内存使用包括(约1Mb的虚拟内存 和 20Kb到60Kb的固定内存),空转状态的Java线程和sleep状态的Java线程在内存占用方面几乎无差别.
Golang协程的内存使用方面(不需要虚拟内存, 2Kb到4Kb的固定内存),空转状态的Golang协程和sleep状态的Golang协程在内存占用方面几乎无差别.
结论: Golang协程在内存开销方面比java线程有优势,开100w的Golang协程需占固定内存2.6G,虚拟内存2.6G。受限于内存大小没办法开100wjava线程
- Java数据
条件 | 虚拟内存消耗 | 每个线程消耗虚拟内存 | 固定内存消耗 | 每个线程消耗固定内存 |
sleep线程(501-1) | 10592640Kb-10073528Kb | 500 * 1038.22Kb | 73420Kb-43312Kb | 500 * 60.216Kb |
sleep线程(1001-501) | 11113744Kb-10592640Kb | 500 * 1042.20Kb | 84368Kb-73420Kb | 500 * 21.896Kb |
sleep线程(1001-1) | 11113744Kb-10073528Kb | 1000 * 1040.21Kb | 84368Kb-43312Kb | 1000 * 41.056Kb |
空转线程(101-1) | 10177000Kb-10073556Kb | 100 * 1034.44Kb | 46828Kb-43528Kb | 100 * 33.00Kb |
空转线程(501-101) | 10592640Kb-10177000Kb | 400 * 1039.10Kb | 54484Kb-46828Kb | 400 * 19.14Kb |
空转线程(501-1) | 10592640Kb-10073556Kb | 500 * 1038.16Kb | 54484Kb-43528Kb | 500 * 21.91Kb |
- Golang数据
条件 | 虚拟内存消耗 | 每个协程消耗虚拟内存 | 固定内存消耗 | 每个协程消耗固定内存 | 问题 |
sleep协程(101-1) | 4976592Kb-4975812Kb | 100 * 7.80Kb | 2900Kb-2568Kb | 100 * 3.32Kb | 无 |
sleep协程(501-1) | 4976592Kb-4975812Kb | 500 * 1.56Kb | 4108Kb-2568Kb | 500 * 3.08Kb | 无 |
sleep协程(1001-501) | 4976592Kb-4976592Kb | 500 * 0Kb | 5452Kb-4108Kb | 500 * 2.69Kb | 无 |
sleep协程(100w-1) | 7636288Kb-4975812Kb | 100w * 2.6Kb | 2613152Kb-2568Kb | 100w * 2.61Kb | 无 |
空转协程(101-1) | 4975556Kb-4975556Kb | 100 * 0Kb | 2816Kb-2532Kb | 100 * 2.84Kb | 无 |
空转协程(501-1) | 4975556Kb-4975556Kb | 500 * 0Kb | 3836Kb-2532Kb | 500 * 2.60Kb | 无 |
空转协程(1001-501) | 4975812Kb-4975556Kb | 500 * 0.51Kb | 5208Kb-3836Kb | 500 * 2.74Kb | 无 |
空转协程(100w-1) | 无参考意义 | 无参考意义 | 无参考意义 | 无参考意义 | 实际没有达到100w协程同时运行 |
系统线程消耗
每个java线程对应1个系统线程,Golang使用的系统线程数很少且不随协程数量变化
结论: Golang协程在系统线程使用方面有优势,如果开启过多的系统线程cpu要用大量的时间做线程切换反而降低了效率
- Java数据
- Golang数据
并发极限,执行效率
因单个协程占用的内存资源更少,所以机器能支持更多的Golang协程创建,所以在并发极限方面Golong 有优势
因减少了系统线程的切换让cpu专注于执行工作,所以Golang在执行效率方面有优势
实际场景
使用java多线程开发时
1 因频繁的线程切换会让整体执行效率降低,应尽量避免创建太多的线程。
2 因长时间占用cpu会让剩余的任务堆积等待,应该尽量避免单个线程长时间占用cpu。
3 使用线程池(享元模式)是一种很好的执行多任务的手段。
golang携程
golang 在网络IO中更具备优势,不过golang协程在某些方面也有问题
例如 磁盘IO是没实现poll方法的,不能用 epoll 池。所以磁盘io没有等待事件, Golang的Goroutine 会卡线程。如果OS 内核线程抽象Machine全部卡住,Go runtime 创建更多的线程来保证一直有可运行的 Machine。这种情况下也会造出像java一样的大量系统线程
展望
- java什么时候也能有协程
OpenJDK 的 JEP 425 :虚拟线程(预览版)功能提案显示:Java 平台将引入虚拟线程特性(期待已久的协程)
有关虚拟线程的更多信息可在 OpenJDK 的 JDK Issue-8277131 中查看,目前该提案于 2021/11/15 创立,目前还处于 JEP 流程的第一阶段,距离稳定版本还需要一段时间。
测试机器机器信息
- 机器年代 :2014
- 操作系统 : MacOS
- 内存 :16G / 1600MHZ / DDR3
*CPU :两核 / Intel Core i5 / 2.6 GHz / L2缓存256KB / L3缓存3MB
测试代码
Java
package org.example;
import com.google.common.base.Stopwatch;
import org.codehaus.plexus.util.StringUtils;
import org.junit.Test;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
enum ThreadType {
RUN_THREAD,
SLEEP_THREAD
}
static class RunThread extends Thread {
private final long runtime;
public RunThread(long runtime) {
this.runtime = runtime;
}
@Override
public void run() {
long startTime = System.currentTimeMillis();
long endTime = startTime + runtime;
while (System.currentTimeMillis() < endTime) {}
}
}
static class SleepThread extends Thread {
private final long runtime;
public SleepThread(long runtime) {
this.runtime = runtime;
}
@Override
public void run() {
try {
Thread.sleep(runtime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void runHoldThread(ThreadType threadType, long holdTime, int number) {
long startTime = System.currentTimeMillis();
System.out.println("start");
Thread[] threadArray = new Thread[number];
for (int i = 0; i < number; i++) {
switch (threadType) {
case RUN_THREAD:
threadArray[i] = new RunThread(holdTime);
break;
case SLEEP_THREAD:
threadArray[i] = new SleepThread(holdTime);
break;
}
}
System.out.println("create complete");
long createThreadOverTime = System.currentTimeMillis();
for (Thread thread : threadArray) {
thread.start();
}
System.out.println("all started");
long callOverTime = System.currentTimeMillis();
for (Thread thread : threadArray) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.printf("调用 耗时:%d ms , 等待耗时: %d ms ,共耗时 %d ms", callOverTime - createThreadOverTime, endTime - callOverTime, endTime-startTime);
}
@Test
public void threadTest() {
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
System.out.printf("MAC 查看进程命令:\nps aux -M %s\ntop -pid %s\n", pid, pid);
// runHoldThread(ThreadType.SLEEP_THREAD, 60000, 1);
// runHoldThread(ThreadType.SLEEP_THREAD, 60000, 101);
// runHoldThread(ThreadType.SLEEP_THREAD, 60000, 501);
// runHoldThread(ThreadType.SLEEP_THREAD, 60000, 1001);
// runHoldThread(ThreadType.RUN_THREAD, 60000, 1);
// runHoldThread(ThreadType.RUN_THREAD, 60000, 101);
// runHoldThread(ThreadType.RUN_THREAD, 60000, 501);
runHoldThread(ThreadType.RUN_THREAD, 60000, 1001);
}
}
Golang
package gmp
import (
"fmt"
"os"
"sync"
"testing"
"time"
)
type RoutineType string
const (
RunRoutine RoutineType = "RunRoutine"
SleepRoutine RoutineType = "SleepRoutine"
)
func TestGoroutine(t *testing.T) {
pid := os.Getpid()
fmt.Printf("MAC 查看进程命令:\nps aux -M %d\ntop -pid %d\n", pid, pid)
//创建 n 个协程,每个空转 m秒
//runGoroutine(SleepRoutine, time.Second*60, 1)
//runGoroutine(SleepRoutine, time.Second*60, 101)
//runGoroutine(SleepRoutine, time.Second*60, 501)
//runGoroutine(SleepRoutine, time.Second*60, 1001)
//runGoroutine(SleepRoutine, time.Second*60, 1000001)
//runGoroutine(RunRoutine, time.Second*60, 1)
//runGoroutine(RunRoutine, time.Second*60, 101)
//runGoroutine(RunRoutine, time.Second*60, 501)
//runGoroutine(RunRoutine, time.Second*60, 1001)
runGoroutine(RunRoutine, time.Second*60, 1000001)
}
//运行协程
func runGoroutine(routineType RoutineType, singleRunningTime time.Duration, number int) {
startTime := time.Now()
var waitGroup sync.WaitGroup
for i := 0; i < number; i++ {
waitGroup.Add(1)
switch routineType {
case RunRoutine:
go runRoutine(singleRunningTime, &waitGroup)
case SleepRoutine:
go sleepRoutine(singleRunningTime, &waitGroup)
}
}
callOverTime := time.Now()
waitGroup.Wait()
runOverTime := time.Now()
callDuration := callOverTime.Sub(startTime).Milliseconds()
waitDuration := runOverTime.Sub(callOverTime).Milliseconds()
fmt.Printf("调用 耗时:%d ms , 等待耗时: %d ms", callDuration, waitDuration)
}
//空转协程
func runRoutine(runningTime time.Duration, waitGroup *sync.WaitGroup) {
defer waitGroup.Done()
timeStart := time.Now()
endTime := timeStart.Add(runningTime)
for time.Now().Before(endTime) {
}
}
//sleep协程
func sleepRoutine(runningTime time.Duration, waitGroup *sync.WaitGroup) {
defer waitGroup.Done()
time.Sleep(runningTime)
}