今天研究的問題:
1. Go并發(fā)憂于Java并發(fā)?
2. Go語言的并發(fā)是多線程實(shí)現(xiàn)的么?
3. Java并發(fā)性能如何提高?
4. 線程的分類:
4. 如何根據(jù)場景選擇不同的線程實(shí)現(xiàn)方式?
一.線程開啟方式對比
場景1:
假設(shè)我們有一個任務(wù),平均執(zhí)行時間為1秒,分別測試一下使用線程和協(xié)程并發(fā)執(zhí)行100000次需要消耗多少時間多律。
go語言
// 線程開啟
// 線程開啟
func testThread(i int) {
fmt.Println("當(dāng)前值:", i )
time.Sleep(time.Second) //延時一秒
}
func test() {
for i := 0; i < 100000; i++ {
go testThread(i)
}
}
func main() {
start := time.Now() // 獲取當(dāng)前時間
test()
elapsed := time.Since(start)
fmt.Println("該函數(shù)執(zhí)行完成耗時:", elapsed)
}
總耗時
java語言
public class TestThread01 extends Thread {
private int i;
public TestThread01(int i) {
this.i = i;
}
public void run() {
System.out.println(this.i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
long startMili = System.currentTimeMillis();// 當(dāng)前時間對應(yīng)的毫秒數(shù)
for (int i = 0; i < 100000; i++) {
new TestThread01(i).start();
}
long endMili = System.currentTimeMillis();//結(jié)束時間
Thread.sleep(7000);
System.out.println("/**總耗時為:" + (endMili - startMili) + "毫秒");
}
}
總耗時
線程池實(shí)現(xiàn)方式
@Configuration
@EnableAsync
public class BeanConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 設(shè)置核心線程數(shù)
executor.setCorePoolSize(10);
// 設(shè)置最大線程數(shù)
executor.setMaxPoolSize(100000);
// 設(shè)置隊列容量
executor.setQueueCapacity(10);
// 設(shè)置線程活躍時間(秒)
executor.setKeepAliveSeconds(10);
// 設(shè)置默認(rèn)線程名稱
executor.setThreadNamePrefix("hello-");
// 設(shè)置拒絕策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任務(wù)結(jié)束后再關(guān)閉線程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
測試類:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppApplication.class)
public class TestThreadPool {
@Autowired
private Print print;
@Test
public void test() throws InterruptedException {
long startMili = System.currentTimeMillis();// 當(dāng)前時間對應(yīng)的毫秒數(shù)
for (int i = 0; i < 100000; i++) {
print.sayHello(i);
}
long endMili = System.currentTimeMillis();//結(jié)束時間
Thread.sleep(1000);
System.out.println("/**總耗時為:" + (endMili - startMili) + "毫秒");
}
}
總耗時:
結(jié)論:
從總體看go語言實(shí)現(xiàn)多線程的方式更為簡潔耗時更短,go關(guān)鍵字輕松實(shí)現(xiàn)
java語言實(shí)現(xiàn)線程相對復(fù)雜,耗時更長
線程池實(shí)現(xiàn)方式需要復(fù)雜配置.
代碼在執(zhí)行過程中CUP占用率: Java > Java線程池 > go語言
- 本質(zhì)原因: go語言開啟的不是線程--->而是協(xié)程(線程中的線程)
原因分析:
分析前需要了解:進(jìn)程-線程-協(xié)程區(qū)別
進(jìn)程空間分配
操作系統(tǒng)采用虛擬內(nèi)存技術(shù)刑巧,把進(jìn)程虛擬地址空間劃分成用戶空間和內(nèi)核空間。
4GB序的進(jìn)程虛擬地址空間被分成兩部分:用戶空間和內(nèi)核空間
用戶空間
用戶空間按照訪問屬性一致的地址空間存放在一起的原則逆皮,劃分成 5個不同的內(nèi)存區(qū)域宅粥。訪問屬性指的是“可讀、可寫电谣、可執(zhí)行等 秽梅。
- 代碼段代碼段是用來存放可執(zhí)行文件的操作指令,可執(zhí)行程序在內(nèi)存中的鏡像剿牺。代碼段需要防止在運(yùn)行時被非法修改企垦,所以只準(zhǔn)許讀取操作,它是不可寫的晒来。
- 數(shù)據(jù)段數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量钞诡,換句話說就是存放程序靜態(tài)分配的變量和全局變量。
- BSS段BSS段包含了程序中未初始化的全局變量潜索,在內(nèi)存中 bss 段全部置零臭增。
- 對 heap堆是用于存放進(jìn)程運(yùn)行中被動態(tài)分配的內(nèi)存段,它的大小并不固定竹习,可動態(tài)擴(kuò)張或縮減誊抛。當(dāng)進(jìn)程調(diào)用malloc等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴(kuò)張)整陌;當(dāng)利用free等函數(shù)釋放內(nèi)存時拗窃,被釋放的內(nèi)存從堆中被剔除(堆被縮減)
- 棧 stack棧是用戶存放程序臨時創(chuàng)建的局部變量瞎领,也就是函數(shù)中定義的變量(但不包括 static 聲明的變量,static意味著在數(shù)據(jù)段中存放變量)随夸。除此以外九默,在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進(jìn)程棧中宾毒,并且待到調(diào)用結(jié)束后驼修,函數(shù)的返回值也會被存放回棧中。由于棧的先進(jìn)后出特點(diǎn)诈铛,所以棧特別方便用來保存/恢復(fù)調(diào)用現(xiàn)場乙各。從這個意義上講,我們可以把堆棿敝瘢看成一個寄存耳峦、交換臨時數(shù)據(jù)的內(nèi)存區(qū)。
-
上述幾種內(nèi)存區(qū)域中數(shù)據(jù)段焕毫、BSS 段蹲坷、堆通常是被連續(xù)存儲在內(nèi)存中,在位置上是連續(xù)的邑飒,而代碼段和棧往往會被獨(dú)立存放循签。堆和棧兩個區(qū)域在 i386 體系結(jié)構(gòu)中棧向下擴(kuò)展、堆向上擴(kuò)展幸乒,相對而生懦底。
內(nèi)核空間
線程
線程是操作操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。線程被包含在進(jìn)程之中罕扎,是進(jìn)程中的實(shí)際運(yùn)作單位聚唐,一個進(jìn)程內(nèi)可以包含多個線程,線程是資源調(diào)度的最小單位腔召。
線程資源和開銷
同一進(jìn)程中的多條線程共享該進(jìn)程中的全部系統(tǒng)資源杆查,如虛擬地址空間,文件描述符文件描述符和信號處理等等臀蛛。但同一進(jìn)程中的多個線程有各自的調(diào)用棧亲桦、寄存器環(huán)境、線程本地存儲等信息浊仆。
線程創(chuàng)建的開銷主要是線程堆棧的建立客峭,分配內(nèi)存的開銷。這些開銷并不大抡柿,最大的開銷發(fā)生在線程上下文切換的時候舔琅。
線程分類
還記得剛開始我們講的內(nèi)核空間和用戶空間概念嗎?線程按照實(shí)現(xiàn)位置和方式的不同洲劣,也分為用戶級線程(協(xié)程)和內(nèi)核線程备蚓,下面一起來看下這兩類線程的差異和特點(diǎn)课蔬。
用戶級線程
實(shí)現(xiàn)在用戶空間的線程稱為用戶級線程。用戶線程是完全建立在用戶空間的線程庫郊尝,用戶線程的創(chuàng)建二跋、調(diào)度、同步和銷毀全由用戶空間的庫函數(shù)完成流昏,不需要內(nèi)核的參與扎即,因此這種線程的系統(tǒng)資源消耗非常低,且非常的高效横缔。
特點(diǎn)
- 用戶線級線程只能參與競爭該進(jìn)程的處理器資源铺遂,不能參與全局處理器資源的競爭。
- 用戶級線程切換都在用戶空間進(jìn)行茎刚,開銷極低。
- 用戶級線程調(diào)度器在用戶空間的線程庫實(shí)現(xiàn)撤逢,內(nèi)核的調(diào)度對象是進(jìn)程本身膛锭,內(nèi)核并不知道用戶線程的存在。
缺點(diǎn)
如果觸發(fā)了引起阻塞的系統(tǒng)調(diào)用的調(diào)用蚊荣,會立即阻塞該線程所屬的整個進(jìn)程初狰。
系統(tǒng)只看到進(jìn)程看不到用戶線程,所以只有一個處理器內(nèi)核會被分配給該進(jìn)程 互例,也就不能發(fā)揮多久 CPU 的優(yōu)勢 奢入。
內(nèi)核級線程
內(nèi)核線程建立和銷毀都是由操作系統(tǒng)負(fù)責(zé)、通過系統(tǒng)調(diào)用完成媳叨,內(nèi)核維護(hù)進(jìn)程及線程的上下文信息以及線程切換腥光。
特點(diǎn)
- 內(nèi)核級線級能參與全局的多核處理器資源分配,充分利用多核 CPU 優(yōu)勢糊秆。
- 每個內(nèi)核線程都可被內(nèi)核調(diào)度武福,因?yàn)榫€程的創(chuàng)建、撤銷和切換都是由內(nèi)核管理的痘番。
- 一個內(nèi)核線程阻塞與他同屬一個進(jìn)程的線程仍然能繼續(xù)運(yùn)行捉片。
缺點(diǎn)
- 內(nèi)核級線程調(diào)度開銷較大。調(diào)度內(nèi)核線程的代價可能和調(diào)度進(jìn)程差不多昂貴伍纫,代價要比用戶級線程大很多。
- 線程表是存放在操作系統(tǒng)固定的表格空間或者堆棸何撸空間里,所以內(nèi)核級線程的數(shù)量是有限的说铃。
什么是協(xié)程
那什么是協(xié)程呢嘹履?攜程 Coroutines 是一種比線程更加輕量級的微線程。類比一個進(jìn)程可以擁有多個線程债热,一個線程也可以擁有多個協(xié)程砾嫉,因此協(xié)程又稱為線程的線程。
線程切換問題:
協(xié)程的調(diào)度完全由用戶控制窒篱,協(xié)程擁有自己的寄存器上下文和棧焕刮,協(xié)程調(diào)度切換時,將寄存器上下文和棧保存到其他地方墙杯,在切回來的時候配并,恢復(fù)先前保存的寄存器上下文和棧,直接操作用戶空間棧高镐,完全沒有內(nèi)核切換的開銷溉旋。
線程切換
協(xié)程切換
GO語言多線程是采用哪種線程類型(GO語言并發(fā)原理)
Golang 在語言層面實(shí)現(xiàn)了對協(xié)程的支持,Goroutine 是協(xié)程在 Go 語言中的實(shí)現(xiàn)嫉髓, 在 Go 語言中每一個并發(fā)的執(zhí)行單元叫作一個 Goroutine 观腊,Go 程序可以輕松創(chuàng)建成百上千個協(xié)程并發(fā)執(zhí)行。
!!!!通過以上分析,我們可以用JAVA語言模仿GO語言的多線程實(shí)現(xiàn)方式--->協(xié)程?
- 有哪些常見的語言支持協(xié)程開發(fā):
python算行, kotlin梧油, javascript 和go - JDK是否支持協(xié)程開發(fā)?
Java 官方目前是還沒推出協(xié)程- 已加入計劃
華為的JDK支持,但并不來源
目前可用性比較高的有 Quasar 和 ea-async 兩個第三方庫,都是通過 byte code Instrument州邢,把編譯后同步程序class文件修改為異步的操作儡陨。
使用JAVA實(shí)現(xiàn)協(xié)程
1.引入Quasar庫
<dependency>
<groupId>co.paralleluniverse</groupId>
<artifactId>quasar-core</artifactId>
<version>0.7.9</version>
<classifier>jdk8</classifier>
</dependency>
代碼實(shí)現(xiàn)
public static void main(String[] args) throws Exception {
long startMili = System.currentTimeMillis();// 當(dāng)前時間對應(yīng)的毫秒數(shù)
for (int i = 0; i < 100000; i++) {
final int count = i;
new Fiber<>((SuspendableCallable<Integer>) () -> {
System.out.println(count);
Fiber.sleep(1000);
return count;
}).start();
}
long endMili = System.currentTimeMillis();//結(jié)束時間
//阻塞等待 協(xié)程執(zhí)行完畢 ----> 可采用阻塞隊列
Thread.sleep(3000);
System.out.println("**總耗時為:" + (endMili - startMili) + "毫秒");
}
那么場景一使用協(xié)程處理速度是多少?
場景二:
用代碼生成1萬個文件放入文件夾對比效率: 這里我只輸出結(jié)果
- go語言: 4.424s
-Java語言: 4.443s
-Java線程池: 3.208s
-Java協(xié)程: 3.614s
結(jié)論:
換個新場景協(xié)程就沒有那么明顯的優(yōu)勢了,所以根據(jù)場景采用不通的線程開啟方式
實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn), 遇到問題建議多采用幾種方式測試
計算強(qiáng) - 建議采用多核, 任務(wù)多,多協(xié)程
使用Java線程池注意事項(慎用線程池):
- 線程池核心設(shè)置參數(shù)
- 核心線程數(shù)與最大線程數(shù)可以不局限于CPU的核心數(shù)(可以根據(jù)業(yè)務(wù)場景調(diào)整)
- 隊列容量設(shè)置理論可以無窮大,單不建議(意思是并發(fā)量多大的時候開啟新的線程)
- 根據(jù)業(yè)務(wù)場景設(shè)置線程拒絕策略