java睡眠后继续执行_Java线程只能有千个,而Go的Goroutine能有上百万个
哈嘍,大家好,我是asong,我又來做知識分享了。
對于做過Java開發的程序員來說,或許會遇到這個問題:java.lang.OutOfMemoryError: Unable to create new native thread。造成這個問題的原因是因為Thread限制導致內存溢出。對于這個問題,我們可以寫一個小demo,測試一下這個問題:
/** * 功能:Unable to create new native thread * 訂閱號:Golang夢工廠 * create by asong on 2020.6.14 * email:741896420@qq.com */public class main {public static void main(String[] args) {while (true){new Thread(new Runnable() {@Override ? ? ? ? ? ? ? ?public void run() {try {Thread.sleep(10000000); ? ? ? ? ? ? ? ? ? ?} catch(InterruptedException e) { }
}
}).start(); ? ? ? ?}
}
}
運行這段代碼就會發生錯誤,在我的電腦上在創建了13000個thread的時候發生了錯誤。但是我們如果使用Go語言嘗試,創建一個Goroutine,并且讓他永久的Sleep,Go語言大概可以創建6千萬個Groutine。我們可以看到Goroutine可以比thread創建多這么多。這一文,我們就來解析這個問題!!!
什么是線程Thread可以由以下內容組成:
1. 一系列按照線性順序可以執行的指令(operations);
2. 一個邏輯上可以執行的路徑。CPUs中的每一個Core個數在同一時刻只能真正并發執行一個Logic thread。
如果你的threads個數大于CPU的Core個數,有一部分的Threads就必須要暫停來讓其他Threads工作,直到這些threads到達一定的時機才會被恢復繼續執行。而暫停和恢復一個線程,至少需要記錄兩件事情:
1. 當前執行的指令位置。亦稱為:說當前線程被暫停時,線程正在執行的代碼行;
2.?還需要一個棧空間。 亦可認為:這個棧空間保存了當前線程的狀態。一個棧包含了 local 變量也就是一些指針指向堆內存的變量(這個是對于 Java 來說的,對于 C/C++ 可以存儲非指針)。一個進程里面所有的 threads 是共享一個堆內存的。
記錄了這兩件事情,CPU在調度thread的時候,就有了足夠的信息,可以暫停一個thread,調度其他thread運行,然后再將暫停的thread恢復,從而繼續執行。這些操作對于thread來說通常是完全透明的。從thread的角度去看,他一直都在連續的運行著。thread被取消調度這樣的行為可以被觀察的唯一辦法就是測量后續操作的時間。
jvm使用的操作系統盡管規范沒有要求所有現代的通用 JVM,在我所知道的范圍內,當前市面上所有的現代通用目的的 JVM 中的 thread 都是被設計成為了操作系統的thread。下面,我將使用“用戶空間 threads" 的概念來指代被語言來調度而不是被操作系統內核調度的 threads。操作系統級別實現的 threads 主要有如下兩點限制:首先限制了 threads 的總數量,其次對于語言層面的 thread 和操作系統層面的 thread 進行 1:1 映射的場景,沒有支持海量并發的解決方案。
jvm中固定大小的棧使用操作系統層面的 thread,每一個 thread 都需要耗費靜態的大量的內存 第二個使用操作系統層面的 thread 所帶來的問題是,每一個 thread 都需要一個固定的棧內存。雖然這個內存大小是可以配置的,但在 64 位的 JVM 環境中,一個 thread 默認使用1MB的棧內存。雖然你可以將默認的棧內存大小改小一點,但是您會權衡內存使用情況, 從而增加堆棧溢出的風險。在你的代碼中遞歸次數越大,越有可能觸發棧溢出。如果使用1MB的棧默認值,那么創建1000個 threads ,將使用 1GB 的 RAM ,雖然 RAM 現在很便宜,但是如果要創建一億個 threads ,就需要T級別的內存。
Golang處理辦法:動態大小的棧Go 語言為了避免是使用過大的棧內存(大部分都是未使用的)導致內存溢出,使用了一個非常聰明的技巧:Go 的棧大小是動態的,隨著存儲的數據大小增長和收縮。這不是一件簡單微小的事情,這個特性經過了好幾個版本的迭代開發[4]。很多其他人的關于 Go 語言的文章中都已經做了詳細的說明,本文不打算在這里討論內部的細節。結果就是新建的一個 Goroutine 實際只占用 4KB 的棧空間。一個棧只占用 4KB,1GB 的內存可以創建 250 萬個 Goroutine,相對于 Java 一個棧占用 1MB 的內存,這的確是一個很大的提高。
JVM中上下文切換的延遲使用操作系統的 threads 的最大能力一般在萬級別,主要消耗是在上下文切換的延遲。
因為 JVM 是使用操作系統的 threads ,也就是說是由操作系統內核進行 threads 的調度。操作系統本身有一個所有正在運行的進程和線程的列表,同時操作系統給它們中的每一個都分配一個“公平”的使用 CPU 的時間片。當內核從一個 thread 切換到另外一個時候,它其實有很多事情需要去做。
新線程或進程的運行必須以世界的視角開始,它可以抽象出其他線程在同一 CPU 上運行的事實。這個問題的關鍵點是上下文的切換大概需要消耗 1-100μ 秒。這個看上去好像不是很耗時,但是在現實中每次平均切換需要消耗10μ秒,如果想讓在一秒鐘內,所有的 threads 都能被調用到,那么 threads 在一個 core 上最多只能有 10 萬個 threads,而事實上這些 threads 自身已經沒有任何時間去做自己的有意義的工作了。
Go 的行為有何不同:在一個操作系統線程上運行多個 Goroutines
Golang 語言本身有自己的調度策略,允許多個 Goroutines 運行在一個同樣的 OS thread 上。既然 Golang 能像內核一樣運行代碼的上下文切換,這樣它就能省下大量的時間來避免從用戶態切換到 ring-0 的內核態再切換回來的過程。但是這只是表面上能看到的,事實上為 Go 語言支持 100 萬的 goroutines,Go 語言其實還做了更多更復雜的事情。
即使 JVM 把 threads 帶到了用戶空間,它依然無法支持百萬級別的 threads ,想象下在你的新的系統中,在 thread 間進行切換只需要耗費100 納秒,即使只做上下文切換,有也只能使 100 萬個 threads 每秒鐘做 10 次上下文的切換,更重要的是,你必須要讓你的 CPU 滿負荷的做這樣的事情。
支持真正的高并發需要另外一種優化思路:當你知道這個線程能做有用的工作的時候,才去調度這個線程!如果你正在運行多線程,其實無論何時,只有少部分的線程在做有用的工作。Go 語言引入了 channel 的機制來協助這種調度機制。如果一個 goroutine 正在一個空的 channel 上等待,那么調度器就能看到這些,并不再運行這個 goroutine 。同時 Go 語言更進了一步。它把很多個大部分時間空閑的 goroutines 合并到了一個自己的操作系統線程上。這樣可以通過一個線程來調度活動的 Goroutine(這個數量小得多),而是數百萬大部分狀態處于睡眠的 goroutines 被分離出來。這種機制也有助于降低延遲。
除非 Java 增加一些語言特性來支持調度可見的功能,否則支持智能調度是不可能實現的。但是你可以自己在“用戶態”構建一個運行時的調度器,來調度何時線程可以工作。其實這就是構成Akka這種數百萬 actors 并發框架的基礎概念。
總結未來,會有越來越多的從操作系統層面的 thread 模型向輕量級的用戶空間級別的 threads 模型遷移發生。從使用角度看,使用高級的并發特性是必須的,也是唯一的需求。這種需求其實并沒有增加過多的的復雜度。如果 Go 語言改用操作系統級別的 threads 來替代目前現有的調度和棧空間自增長的機制,其實也就是在 runtime 的代碼包中減少數千行的代碼。但對于大多數的用戶案例上考慮,這是一個更好的的模式。復雜度被語言庫的作者做了很好的抽象,這樣軟件工程師就可以寫出高并發的程序了。
今日的分享到此結束了。感謝各位的觀看,如果有錯誤歡迎指出。
歡迎關注公眾號:Golang夢工廠,后臺回復Gin獲取2020最新版本Gin中文文檔。
訂閱號:Golang夢工廠-掃碼關注公眾號后臺回復gin
?獲取2020最新版本Gin中文文
?檔-
總結
以上是生活随笔為你收集整理的java睡眠后继续执行_Java线程只能有千个,而Go的Goroutine能有上百万个的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为怎么显示返回按键_华为 iateey
- 下一篇: python下载图片被覆盖了_scrap