java线程和go语言的goroutine的差异
java和go我平常都经常会写,这两种语言各有各的特点。java的优势就是生态好,而且java每次升级都有一些惊喜,例如最新的java21,更新了一种轻量级线程特性,对标go的goroutine,持续迭代实用的特性是java长久不衰最重要的原因之一。
今天讲讲goroutine比java线程好在哪里,也是为什么java21要出一个轻量级线程的原因。
Java Thread
java的线程是一对一映射内核线程,每创建一个java线程,都需要再操作系统层面,创建一个内核线程。并且java线程的调度,完全依赖操作系统的调度策略。
![image](https://prod-files-secure.s3.us-west-2.amazonaws.com/371abca5-94fd-4d13-a43e-bbcb27be7c63/7819a345-d1f2-4760-b165-b5de91623293/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466T43FGCV7%2F20250205%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250205T155611Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEC8aCXVzLXdlc3QtMiJGMEQCIG2lHcylVbMvpjLk3qKNV9cgsYEpQJENT7SmM9Ie%2F948AiBd4Wph1kypl4VmmteAgkBi%2FEcC7rECxWXarN6oDmUEQCr%2FAwhIEAAaDDYzNzQyMzE4MzgwNSIMSpXWvm%2F0x7JD10DpKtwDNNbO2aWBiUte%2BuZgYnNFYuCN9nzaAEUFZXtA91TYrsJBRjj36uPCA0LaGLqlGkSgApBwg7RSJDCpkXl9n48%2FCiDIxVy0uGRINn6Cm6bnkyHInHnsdfuOBqMGMy3x%2FW54TTPs6MzC2%2FOJfF1ex5VabwQCyBT7poBULYjQI9v8%2BKDmAGDLPafPqacixRQT4ZGzN9he8sNJsK6KilfddKLzEyARUx7XlZf66HPcCsdiYHqXkITHwgq%2B2UR0ZGHamIFBP4UrlZu5ZBvoAWCCH4xMRWPwszav2%2B%2FJ8%2BI0jSisSfTtkAMtUCQ9EzxpksqmNsxV3DwY9avP4adUXuf0%2FjCzrP%2F6c9UjPUeXnrmoA4DPx1rtX0LNY9rKoe7Vr4VVITHdI3Kb%2F%2BXK87eRNk5SEg2obel2Xpf19%2FgV%2FE%2FsKyDeKmt5UaNSXm9Eyhit%2BmkagEvyx818lMjULFFGc7sIVoXCxKp1sr4oYVZRc7VHSpBAr3Fprhucn8X1M1yf%2FzuHel4rBtLXkkbaBiiljwjwRZsyMW6Glr7TVuQGudVfaMoChe2ycWK%2F8bUSC8J%2FydwAd%2BgIu5AckrAUV%2B05JZMEsa07qi38Q05SOYLV%2FVlfPZSSh42dSTJNUVygimb0qMYw9oCOvQY6pgH6k2%2Fhk%2Bt0JpG3mihUDe0U7l9PWoTKq9eg6nc84FBRDXOE1Zmt6d%2BL%2B63HGK%2BgknshfNoQNz7WLg7UijkYAABg7n9S8FUAQ%2Fq%2FOOcpcZ6pAF%2B4lt5hr%2BvKzooqCGJyY8qyM7FR1c76cvQXsMAKh%2B9YSUWcmGW43FOcCV3wiZRKrdOMY%2F6TKphPpTicEemAZt9lTme4%2BCEXX0lZmMyPnVK68AHWA2Cw&X-Amz-Signature=7a2b6d761e1eea7a077aaaa0de7f68982e5187b5731036cd69a3847239f947d9&X-Amz-SignedHeaders=host&x-id=GetObject)
这种依赖内核线程和操作系统调度的实现方式有如下问题:
Goroutine
和java线程不同的是,goroutine和内核线程不是一对一的映射关系。go实现了自己的调度器,然后将一个内核线程映射一组goroutine,java线程和内核线程是N比N的关系,goroutine则是N比M的关系,M可以远远大于N。
![image](https://prod-files-secure.s3.us-west-2.amazonaws.com/371abca5-94fd-4d13-a43e-bbcb27be7c63/6d48ac1a-706b-42ea-8974-0941b2e1a4ff/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466T43FGCV7%2F20250205%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250205T155611Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEC8aCXVzLXdlc3QtMiJGMEQCIG2lHcylVbMvpjLk3qKNV9cgsYEpQJENT7SmM9Ie%2F948AiBd4Wph1kypl4VmmteAgkBi%2FEcC7rECxWXarN6oDmUEQCr%2FAwhIEAAaDDYzNzQyMzE4MzgwNSIMSpXWvm%2F0x7JD10DpKtwDNNbO2aWBiUte%2BuZgYnNFYuCN9nzaAEUFZXtA91TYrsJBRjj36uPCA0LaGLqlGkSgApBwg7RSJDCpkXl9n48%2FCiDIxVy0uGRINn6Cm6bnkyHInHnsdfuOBqMGMy3x%2FW54TTPs6MzC2%2FOJfF1ex5VabwQCyBT7poBULYjQI9v8%2BKDmAGDLPafPqacixRQT4ZGzN9he8sNJsK6KilfddKLzEyARUx7XlZf66HPcCsdiYHqXkITHwgq%2B2UR0ZGHamIFBP4UrlZu5ZBvoAWCCH4xMRWPwszav2%2B%2FJ8%2BI0jSisSfTtkAMtUCQ9EzxpksqmNsxV3DwY9avP4adUXuf0%2FjCzrP%2F6c9UjPUeXnrmoA4DPx1rtX0LNY9rKoe7Vr4VVITHdI3Kb%2F%2BXK87eRNk5SEg2obel2Xpf19%2FgV%2FE%2FsKyDeKmt5UaNSXm9Eyhit%2BmkagEvyx818lMjULFFGc7sIVoXCxKp1sr4oYVZRc7VHSpBAr3Fprhucn8X1M1yf%2FzuHel4rBtLXkkbaBiiljwjwRZsyMW6Glr7TVuQGudVfaMoChe2ycWK%2F8bUSC8J%2FydwAd%2BgIu5AckrAUV%2B05JZMEsa07qi38Q05SOYLV%2FVlfPZSSh42dSTJNUVygimb0qMYw9oCOvQY6pgH6k2%2Fhk%2Bt0JpG3mihUDe0U7l9PWoTKq9eg6nc84FBRDXOE1Zmt6d%2BL%2B63HGK%2BgknshfNoQNz7WLg7UijkYAABg7n9S8FUAQ%2Fq%2FOOcpcZ6pAF%2B4lt5hr%2BvKzooqCGJyY8qyM7FR1c76cvQXsMAKh%2B9YSUWcmGW43FOcCV3wiZRKrdOMY%2F6TKphPpTicEemAZt9lTme4%2BCEXX0lZmMyPnVK68AHWA2Cw&X-Amz-Signature=e376fef2d8c94a7d150b417d239e47de1c0a6cd250322e860b83016901a33935&X-Amz-SignedHeaders=host&x-id=GetObject)
如上图所示,每一个内核线程都映射了一个goroutine队列,这个队列是一个FIFO队列。每一个线程都会每隔10ms将当前执行的goroutine放回队列,然后从队列里读取一个新的goroutine,这样就完成了一次公平的切换操作。
线程不仅仅可以从自己的goroutine队列里读取goroutine,还可以读取全局队列里面的goroutine,甚至还可以读取其他线程的goroutine,这个操作称为work-stealing 。
go使用GOMAXPROCS参数来设置线程的数量,也就是N比M中N的数量,默认值为本机的核心数量。
动态伸缩的栈空间
和java中使用的固定大小的栈空间策略不一样的是,go最开始给每一个goroutine分配4k的空间,如果goroutine运行过程中要使用超过4k的空间,go会动态的扩容这个goroutine的占空间。
动态创建线程
你可能会想到,如果某个线程因为系统调用被阻塞了,例如读取磁盘操作,那整个goroutine队列不都被阻塞住了吗?
![image](https://prod-files-secure.s3.us-west-2.amazonaws.com/371abca5-94fd-4d13-a43e-bbcb27be7c63/002cf845-bf07-4130-92d9-3f11ba2a5693/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466T43FGCV7%2F20250205%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250205T155611Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEC8aCXVzLXdlc3QtMiJGMEQCIG2lHcylVbMvpjLk3qKNV9cgsYEpQJENT7SmM9Ie%2F948AiBd4Wph1kypl4VmmteAgkBi%2FEcC7rECxWXarN6oDmUEQCr%2FAwhIEAAaDDYzNzQyMzE4MzgwNSIMSpXWvm%2F0x7JD10DpKtwDNNbO2aWBiUte%2BuZgYnNFYuCN9nzaAEUFZXtA91TYrsJBRjj36uPCA0LaGLqlGkSgApBwg7RSJDCpkXl9n48%2FCiDIxVy0uGRINn6Cm6bnkyHInHnsdfuOBqMGMy3x%2FW54TTPs6MzC2%2FOJfF1ex5VabwQCyBT7poBULYjQI9v8%2BKDmAGDLPafPqacixRQT4ZGzN9he8sNJsK6KilfddKLzEyARUx7XlZf66HPcCsdiYHqXkITHwgq%2B2UR0ZGHamIFBP4UrlZu5ZBvoAWCCH4xMRWPwszav2%2B%2FJ8%2BI0jSisSfTtkAMtUCQ9EzxpksqmNsxV3DwY9avP4adUXuf0%2FjCzrP%2F6c9UjPUeXnrmoA4DPx1rtX0LNY9rKoe7Vr4VVITHdI3Kb%2F%2BXK87eRNk5SEg2obel2Xpf19%2FgV%2FE%2FsKyDeKmt5UaNSXm9Eyhit%2BmkagEvyx818lMjULFFGc7sIVoXCxKp1sr4oYVZRc7VHSpBAr3Fprhucn8X1M1yf%2FzuHel4rBtLXkkbaBiiljwjwRZsyMW6Glr7TVuQGudVfaMoChe2ycWK%2F8bUSC8J%2FydwAd%2BgIu5AckrAUV%2B05JZMEsa07qi38Q05SOYLV%2FVlfPZSSh42dSTJNUVygimb0qMYw9oCOvQY6pgH6k2%2Fhk%2Bt0JpG3mihUDe0U7l9PWoTKq9eg6nc84FBRDXOE1Zmt6d%2BL%2B63HGK%2BgknshfNoQNz7WLg7UijkYAABg7n9S8FUAQ%2Fq%2FOOcpcZ6pAF%2B4lt5hr%2BvKzooqCGJyY8qyM7FR1c76cvQXsMAKh%2B9YSUWcmGW43FOcCV3wiZRKrdOMY%2F6TKphPpTicEemAZt9lTme4%2BCEXX0lZmMyPnVK68AHWA2Cw&X-Amz-Signature=ee1b3804cf8ccf04c48edcd5b04a6722dbbb7be371db528d9e47a042cc46f914&X-Amz-SignedHeaders=host&x-id=GetObject)
上图中的场景就是,大部分内核线程都被系统调用阻塞住了,只有一个内核线程是可执行的,这种情况下程序的性能肯定会大打折扣。
go设计一个一个动态创建线程的特性来防止上面的情况出现。当一个线程被阻塞了,go会创建一个新的线程来继续处理这个被阻塞线程对应的goroutine队列,并且会立即把执行系统调用线程的控制权交给这个新创建的线程。
![image](https://prod-files-secure.s3.us-west-2.amazonaws.com/371abca5-94fd-4d13-a43e-bbcb27be7c63/d923ea55-aa1e-45ac-9505-5d20abb081c2/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466T43FGCV7%2F20250205%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250205T155611Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEC8aCXVzLXdlc3QtMiJGMEQCIG2lHcylVbMvpjLk3qKNV9cgsYEpQJENT7SmM9Ie%2F948AiBd4Wph1kypl4VmmteAgkBi%2FEcC7rECxWXarN6oDmUEQCr%2FAwhIEAAaDDYzNzQyMzE4MzgwNSIMSpXWvm%2F0x7JD10DpKtwDNNbO2aWBiUte%2BuZgYnNFYuCN9nzaAEUFZXtA91TYrsJBRjj36uPCA0LaGLqlGkSgApBwg7RSJDCpkXl9n48%2FCiDIxVy0uGRINn6Cm6bnkyHInHnsdfuOBqMGMy3x%2FW54TTPs6MzC2%2FOJfF1ex5VabwQCyBT7poBULYjQI9v8%2BKDmAGDLPafPqacixRQT4ZGzN9he8sNJsK6KilfddKLzEyARUx7XlZf66HPcCsdiYHqXkITHwgq%2B2UR0ZGHamIFBP4UrlZu5ZBvoAWCCH4xMRWPwszav2%2B%2FJ8%2BI0jSisSfTtkAMtUCQ9EzxpksqmNsxV3DwY9avP4adUXuf0%2FjCzrP%2F6c9UjPUeXnrmoA4DPx1rtX0LNY9rKoe7Vr4VVITHdI3Kb%2F%2BXK87eRNk5SEg2obel2Xpf19%2FgV%2FE%2FsKyDeKmt5UaNSXm9Eyhit%2BmkagEvyx818lMjULFFGc7sIVoXCxKp1sr4oYVZRc7VHSpBAr3Fprhucn8X1M1yf%2FzuHel4rBtLXkkbaBiiljwjwRZsyMW6Glr7TVuQGudVfaMoChe2ycWK%2F8bUSC8J%2FydwAd%2BgIu5AckrAUV%2B05JZMEsa07qi38Q05SOYLV%2FVlfPZSSh42dSTJNUVygimb0qMYw9oCOvQY6pgH6k2%2Fhk%2Bt0JpG3mihUDe0U7l9PWoTKq9eg6nc84FBRDXOE1Zmt6d%2BL%2B63HGK%2BgknshfNoQNz7WLg7UijkYAABg7n9S8FUAQ%2Fq%2FOOcpcZ6pAF%2B4lt5hr%2BvKzooqCGJyY8qyM7FR1c76cvQXsMAKh%2B9YSUWcmGW43FOcCV3wiZRKrdOMY%2F6TKphPpTicEemAZt9lTme4%2BCEXX0lZmMyPnVK68AHWA2Cw&X-Amz-Signature=22a188e7c9fd7afdc116560569a4e773dd77ebe642e9367aaf8cead3ceb275ed&X-Amz-SignedHeaders=host&x-id=GetObject)
如果你是华生,你一定发现了盲点。如果一个go程序大量使用System Call,那这个程序会创建大量的内核线程,导致goroutine会降级成java thread。
总结
对于java线程,goroutine在绝大部分场景下,确实会轻量级很多,低成本的上下文切换和动态的栈空间使goroutine在应付绝大部分常规的并发场景都非常完美,虽然某些极端情况下,例如大量的系统调用场景,会导致goroutine失去它的优势,但是总体来讲,goroutine是一个非常好的并发编程解决方案。这也是为什么java21要在上轻量级线程的原因,善于学习其他语言的优秀特性相比于固步自封,更加值得点赞。
参考:https://medium.com/@genchilu/javas-thread-model-and-golang-goroutine-f1325ca2df0c