本文不从语言角度谈论好与不好。本文从性能测试角度分析一下Java线程与Golang协程的区别

用例设计

java 实现多线程任务处理:启动一定数量的等待线程或空转线程,并让启动的线程维持固定时间(60秒)
golang实现多协程任务处理:启动一定数量的等待协程或空转协程,并让启动的协程维持固定时间(60秒)

测试结果

Java

golang比java快 golang 和java运行效率对比_开发语言

Golang

golang比java快 golang 和java运行效率对比_golang_02

结果分析

内存使用

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)
}