JavaEE-多线程(基础篇一)
文章目錄
- 線程是啥?!(Thread)
- Java的線程與操作系統(tǒng)線程的關(guān)系
- 操作系統(tǒng)線程模型
- 線程-在用戶空間下實(shí)現(xiàn)
- 線程實(shí)現(xiàn)在操作系統(tǒng)內(nèi)核中
- 使用用戶線程更加輕量級進(jìn)程混合實(shí)現(xiàn)
- Java線程
- Java線程在操作系統(tǒng)上本質(zhì)
- Java中的線程
- 操作系統(tǒng)中的進(jìn)程(線程)狀態(tài)(區(qū)分和JVM中的線程狀態(tài))
- 操作系統(tǒng)中線程和Java線程狀態(tài)的關(guān)系
- 為哈要有線程?
- 線程的優(yōu)點(diǎn)
- 進(jìn)程和線程的區(qū)別
- 創(chuàng)建一個(gè)多線程程序
- 創(chuàng)建線程
- 法一-創(chuàng)建Thread子類
- 法二-實(shí)現(xiàn)Runnable接口
- 法三-匿名內(nèi)部類創(chuàng)建Thread子類對象
- 法四-匿名內(nèi)部類創(chuàng)建Runnable子類對象
- 法五-lambda表達(dá)式創(chuàng)建Runnable子類對象
- Thread的run和start的區(qū)別
- 多線程的使用場景
上一篇博客,為大家詳細(xì)得講解了進(jìn)程,那么本篇,主要就開始研究線程,以及在Java中是如何進(jìn)行多線程并發(fā)編程的.
本篇博客目標(biāo)在于帶大家理解和認(rèn)識多線程,然后進(jìn)一步掌握多線程程序的編寫和狀態(tài),認(rèn)識什么是線程不安全,以及其對應(yīng)的解決思路,災(zāi)后掌握一系列的關(guān)鍵字
線程是啥?!(Thread)
OK,為了保持本篇博客的嚴(yán)謹(jǐn)性,我們先來非常offensive一下,去查一查官方的解釋
給大家總結(jié)一下.
線程是程序運(yùn)行的基本執(zhí)行單元。當(dāng)操作系統(tǒng)(不包括單線程的操作系統(tǒng),如微軟早期的DOS)在執(zhí)行一個(gè)程序時(shí),會(huì)在系統(tǒng)中建立一個(gè)進(jìn)程,而在這個(gè)進(jìn)程中,必須至少建立一個(gè)線程(這個(gè)線程被稱為主線程)來作為這個(gè)程序運(yùn)行的入口點(diǎn)。因此,在操作系統(tǒng)中運(yùn)行的任何程序都至少有一個(gè)主線程。
進(jìn)程和線程是現(xiàn)代操作系統(tǒng)中兩個(gè)必不可少的運(yùn)行模型。在操作系統(tǒng)中可以有多個(gè)進(jìn)程,這些進(jìn)程包括系統(tǒng)進(jìn)程(由操作系統(tǒng)內(nèi)部建立的進(jìn)程)和用戶進(jìn)程(由用戶程序建立的進(jìn)程);一個(gè)進(jìn)程中可以有一個(gè)或多個(gè)線程。進(jìn)程和進(jìn)程之間不共享內(nèi)存,也就是說系統(tǒng)中的進(jìn)程是在各自獨(dú)立的內(nèi)存空間中運(yùn)行的。而一個(gè)進(jìn)程中的線可以共享系統(tǒng)分派給這個(gè)進(jìn)程的內(nèi)存空間。
線程不僅可以共享進(jìn)程的內(nèi)存,而且還擁有一個(gè)屬于自己的內(nèi)存空間,這段內(nèi)存空間也叫做線程棧, 是在建立線程時(shí)由系統(tǒng)分配的,主要用來保存線程內(nèi)部所使用的數(shù)據(jù),如線程執(zhí)行函數(shù)中所定義的變量。
注意:任何一個(gè)線程在建立時(shí)都會(huì)執(zhí)行一個(gè)函數(shù),這個(gè)函數(shù)叫做線程執(zhí)行函數(shù)。也可以將這個(gè)函數(shù)看做線程的入口點(diǎn)(類似于程序中的main函數(shù))。無論使用什么語言或技術(shù)來建立線程,都必須執(zhí)行這個(gè)函數(shù)(這個(gè)函數(shù)的表現(xiàn)形式可能不一樣,但都會(huì)有一個(gè)這樣的函數(shù))。如在Windows中用于建立線程的API函數(shù)CreateThread的第三個(gè)參數(shù)就是這個(gè)執(zhí)行函數(shù)的指針。
在操作系統(tǒng)將進(jìn)程分成多個(gè)線程后,這些線程可以在操作系統(tǒng)的管理下并發(fā)執(zhí)行,從而大大提高了程序的運(yùn)行效率。雖然線程的執(zhí)行從宏觀上看是多個(gè)線程同時(shí)執(zhí)行,但實(shí)際上這只是操作系統(tǒng)的障眼法。由于一塊CPU同時(shí)只能執(zhí)行一條指令,因此,在擁有一塊CPU的計(jì)算機(jī)上不可能同時(shí)執(zhí)行兩個(gè)任務(wù)。而操作系統(tǒng)為了能提高程序的運(yùn)行效率,在一個(gè)線程空閑時(shí)會(huì)撤下這個(gè)線程,并且會(huì)讓其他的線程來執(zhí)行,這種方式叫做線程調(diào)度。我們之所以從表面上看是多個(gè)線程同時(shí)執(zhí)行,是因?yàn)椴煌€程之間切換的時(shí)間非常短,而且在一般情況下切換非常頻繁。假設(shè)我們有線程A和B。在運(yùn)行時(shí),可能是A執(zhí)行了1毫秒后,切換到B后,B又執(zhí)行了1毫秒,然后又切換到了A,A又執(zhí)行1毫秒。由于1毫秒的時(shí)間對于普通人來說是很難感知的,因此,從表面看上去就象A和B同時(shí)執(zhí)行一樣,但實(shí)際上A和B是交替執(zhí)行的。
以上便是整理出的線程概念,初學(xué)的小白可能很難看得明白
那么現(xiàn)在我們就對這些非常官方的東西做一些比較人性化的解釋
首先,線程和進(jìn)程之間有一定的練習(xí),那么為什么要有多個(gè)進(jìn)程呢?
答:是為了并發(fā)編程!但是CPU單個(gè)核心已經(jīng)發(fā)展到了極致,如果想要提升算力,就得使用多個(gè)核心,那么我們引入并發(fā)編程,最大的目的就是為了能夠充分的利用好CPU的多核資源.
使用多進(jìn)程這種編程模型,是完全可以做到并發(fā)編程的,并且也能夠使CPU多核被充分利用.但是在有些場景下,又會(huì)產(chǎn)生一些問題:如果需要頻繁地創(chuàng)建/銷毀進(jìn)程,這個(gè)時(shí)候就會(huì)比較低效.
那么創(chuàng)建和銷毀進(jìn)程有什么應(yīng)用背景呢?比如,你寫了一個(gè)服務(wù)器程序,服務(wù)器要同一時(shí)刻給很多客戶端提供服務(wù),這個(gè)時(shí)候就需要并發(fā)編程了,典型地做法,就是每個(gè)客戶端分配一個(gè)進(jìn)程,提供一對一的服務(wù)
創(chuàng)建/銷毀進(jìn)程,本身就是一個(gè)比較低效的操作:
為了提高這個(gè)場景下的效率,我們就引入了"線程",線程其實(shí)也叫做"輕量級進(jìn)程"
一個(gè)線程其實(shí)是包含在進(jìn)程中的,一個(gè)進(jìn)程里面可以有很多個(gè)線程.每個(gè)線程也有自己的PCB(一個(gè)進(jìn)程里面可能就對應(yīng)多個(gè)PCB)同一個(gè)進(jìn)程里面的多個(gè)線程之間,共用一份系統(tǒng)資源,這就意味著,新創(chuàng)建的線程,可以不必重新給他分配系統(tǒng)資源,只需要復(fù)用之前的就可以了.
因此創(chuàng)建線程只需要:
這就是線程相對于進(jìn)程做出的重大改進(jìn),也就是線程更加輕量的原因
進(jìn)程是系統(tǒng)分配資源的最小單位,線程是系統(tǒng)調(diào)度的最小單位.
Java的線程與操作系統(tǒng)線程的關(guān)系
操作系統(tǒng)線程模型
線程-在用戶空間下實(shí)現(xiàn)
當(dāng)線程在用戶空間下實(shí)現(xiàn)的時(shí)候,操作系統(tǒng)對線程的存在并不知曉,操作系統(tǒng)只能看到進(jìn)程,而不能看到進(jìn)程.所有的線程都是在用戶控件實(shí)現(xiàn)的.在操作系統(tǒng)看來,每一個(gè)進(jìn)程只有一個(gè)線程,過去的操作系統(tǒng)大部分是這種實(shí)現(xiàn)方式,這種方式的好處之一是即使操作系統(tǒng)不支持線程,也可以通過庫函數(shù)來支持線程.
那么我們用更通俗的方式來講解這段話.在這種模型下,程序員需要自己實(shí)現(xiàn)線程的數(shù)據(jù)結(jié)構(gòu),創(chuàng)建銷毀和調(diào)度維護(hù).也就相當(dāng)于需要實(shí)現(xiàn)一個(gè)自己的線程調(diào)度內(nèi)核,而同時(shí)這些線程運(yùn)行在操作系統(tǒng)的一個(gè)進(jìn)程內(nèi),最后操作系用直接對進(jìn)程進(jìn)行調(diào)度
這樣做是由一些有點(diǎn)的,首先就是確實(shí)在操作系統(tǒng)種實(shí)現(xiàn)了真實(shí)的多線程,其次線程的調(diào)度只是在用戶態(tài),減少了操作系統(tǒng)從內(nèi)核態(tài)到用戶態(tài)的切換開銷
當(dāng)然,缺點(diǎn)也是很明顯的.
這種模式最致命的缺點(diǎn)也是由于操作系統(tǒng)不知道線程的存在,因此當(dāng)一個(gè)進(jìn)程中的某一個(gè)線程進(jìn)行系統(tǒng)調(diào)用時(shí),比如缺頁中斷而導(dǎo)致線程阻塞,此時(shí)操作系統(tǒng)會(huì)阻塞整個(gè)進(jìn)程,即使這個(gè)進(jìn)程中其它線程還在工作。還有一個(gè)問題是假如進(jìn)程中一個(gè)線程長時(shí)間不釋放CPU,因?yàn)橛脩艨臻g并沒有時(shí)鐘中斷機(jī)制,會(huì)導(dǎo)致此進(jìn)程中的其它線程得不到CPU而持續(xù)等待。
線程實(shí)現(xiàn)在操作系統(tǒng)內(nèi)核中
內(nèi)核線程就是直接由操作系統(tǒng)內(nèi)核支持的線程,這種線程由內(nèi)核來完成內(nèi)核切換,內(nèi)核通過操縱調(diào)度器隊(duì)線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射在各個(gè)處理器上.每個(gè)內(nèi)核線程可以視為內(nèi)核的一個(gè)分身,這樣操作系統(tǒng)就有能力同時(shí)處理多件事情,支持多線程的內(nèi)核就叫做多線程內(nèi)核
通俗的講,程序員直接使用操作系統(tǒng)中已經(jīng)實(shí)現(xiàn)的線程,而線程的創(chuàng)建,銷毀,調(diào)度,和維護(hù),都是靠操作系統(tǒng)(準(zhǔn)確的說是內(nèi)核)來實(shí)現(xiàn),程序員只需要使用系統(tǒng)調(diào)用,而不需要自己設(shè)計(jì)線程的調(diào)度算法和線程隊(duì)CPU資源的搶占使用
輕量級進(jìn)程(LWP)是建立在內(nèi)核之上并由內(nèi)核支持的用戶線程,它是內(nèi)核線程的高度抽象,每一個(gè)輕量級進(jìn)程都與一個(gè)特定的內(nèi)核線程關(guān)聯(lián).內(nèi)核線程只能由內(nèi)核管理并像普通進(jìn)程一樣被調(diào)度
這種輕量級進(jìn)程與內(nèi)核線程之間1:1的 關(guān)系就稱為一對一的線程模型
使用用戶線程更加輕量級進(jìn)程混合實(shí)現(xiàn)
在這種混合實(shí)現(xiàn)下,即存在用戶線程,也存在輕量級線程.用戶線程還是可以完全建立在用戶空間中,因此用戶線程的創(chuàng)建,切換,析構(gòu)等操作依然十分廉價(jià),并且可以支持大規(guī)模的用戶線程并發(fā).而操作系統(tǒng)提供支持的輕量級進(jìn)程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程線程調(diào)度共嗯那個(gè)及處理器映射,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級進(jìn)程來完成,大大降低了整個(gè)進(jìn)程被完全阻塞的風(fēng)險(xiǎn).在這種混合模式中,用戶線程與輕量級進(jìn)程的數(shù)量比是不定的
明白了前面兩種模型,就很好理解這種線程模型了,但實(shí)際上現(xiàn)在主流的操作系統(tǒng)已經(jīng)不太常用這種線程模型了
Java線程
Java線程在操作系統(tǒng)上本質(zhì)
Java線程在JDK1.2之前,是基于稱為綠色線程的用戶線程實(shí)現(xiàn)的,而在JDK1.2中,線程模型替換為基于操作系統(tǒng)原生線程模型來實(shí)現(xiàn).因此,在目前的我JDK版本中,操作系統(tǒng)支持怎樣的線程模型,在很大程度上決定了Java虛擬機(jī)的線程是怎樣映射的,這單在不同的平臺沒有辦法達(dá)成一致,虛擬機(jī)規(guī)范中也并未限定Java線程需要使用哪種線程模型來實(shí)現(xiàn).線程模型支隊(duì)線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響,隊(duì)Java程序的編碼和運(yùn)行過程來說,這些差異都是透明的.
也就是說程序員們?yōu)镴VM開發(fā)了自己的一個(gè)線程調(diào)度內(nèi)核,而到操作系統(tǒng)層面就是用戶空間內(nèi)的線程實(shí)現(xiàn).而到了JDK1.2以后,JVM選擇了更加穩(wěn)健且方便使用的操作系統(tǒng)原生的線程模型,通過系統(tǒng)調(diào)用,將程序的線程交給了操作系統(tǒng)內(nèi)核進(jìn)行調(diào)度
也就是說,現(xiàn)在的Java中線程的本質(zhì),也就是操作系統(tǒng)中的線程,Linux下是基于pthread庫實(shí)現(xiàn)的輕量級進(jìn)程,Windows下是原生的系統(tǒng)win32API提供系統(tǒng)調(diào)用從而實(shí)現(xiàn)多線程
Java中的線程
特別注意:這些線程的狀態(tài)時(shí)JVM中的線程狀態(tài)!不是操作系統(tǒng)中的線程狀態(tài)。
操作系統(tǒng)中的進(jìn)程(線程)狀態(tài)(區(qū)分和JVM中的線程狀態(tài))
這里需要著重解釋一點(diǎn),再現(xiàn)在的操作系統(tǒng)中,因?yàn)榫€程依舊被視為輕量級進(jìn)程,所以操作系統(tǒng)中線程的狀態(tài)實(shí)際上和進(jìn)程狀態(tài)時(shí)一致的模型
操作系統(tǒng)中線程和Java線程狀態(tài)的關(guān)系
從實(shí)際意義上來講,操作系統(tǒng)中的線程除去new和terminated狀態(tài),一個(gè)和線程真實(shí)存在的狀態(tài),只有
- ready:表示線程已經(jīng)被創(chuàng)建,正在等待系統(tǒng)調(diào)度分配CPU使用權(quán)
- running:表示線程獲得了CPU使用權(quán),正在運(yùn)行
- waiting:表示線程等待(或者說掛起),讓出CPU資源給其他線程使用
那么為什么除去new和terminated狀態(tài)呢?是因?yàn)檫@兩種狀態(tài)實(shí)際上并不存在于線程運(yùn)行中,所以也沒什么實(shí)際討論的意義
對于Java中的線程狀態(tài):
無論是Timed Waiting ,Waiting還是Blocked,對應(yīng)的都是操作系統(tǒng)線程的waiting(等待)狀態(tài)
而Runnable狀態(tài),則對應(yīng)了操作系統(tǒng)中的ready和running狀態(tài)
而對不同的操作系統(tǒng),由于本身設(shè)計(jì)思路不一樣,對于線程的設(shè)計(jì)也存在差異,所以JVM在設(shè)計(jì)的時(shí)候已經(jīng)聲明:虛擬機(jī)中的線程狀態(tài),不反應(yīng)任何操作系統(tǒng)線程狀態(tài).
為哈要有線程?
進(jìn)程屬于在CPU和系統(tǒng)資源等方面提供的抽象,能夠有效提高CPU的利用率。
線程是在進(jìn)程這個(gè)層次上提供的一層并發(fā)的抽象:
進(jìn)程有很多優(yōu)點(diǎn),它提供了多道編程,讓我們感覺我們每個(gè)人都擁有自己的CPU和其他資源,可以提高計(jì)算機(jī)的利用率。很多人就不理解了,既然進(jìn)程這么優(yōu)秀,為什么還要線程呢?其實(shí),仔細(xì)觀察就會(huì)發(fā)現(xiàn)進(jìn)程還是有很多缺陷的,主要體現(xiàn)在兩點(diǎn)上:
進(jìn)程只能在一個(gè)時(shí)間干一件事,如果想同時(shí)干兩件事或多件事,進(jìn)程就無能為力了。
進(jìn)程在執(zhí)行的過程中如果阻塞,例如等待輸入,整個(gè)進(jìn)程就會(huì)掛起,即使進(jìn)程中有些工作不依賴于輸入的數(shù)據(jù),也將無法執(zhí)行。
如果這兩個(gè)缺點(diǎn)理解比較困難的話,舉個(gè)現(xiàn)實(shí)的例子也許你就清楚了:如果把我們上課的過程看成一個(gè)進(jìn)程的話,那么我們要做的是耳朵聽老師講課,手上還 要記筆記,腦子還要思考問題,這樣才能高效的完成聽課的任務(wù)。而如果只提供進(jìn)程這個(gè)機(jī)制的話,上面這三件事將不能同時(shí)執(zhí)行,同一時(shí)間只能做一件事,聽的時(shí) 候就不能記筆記,也不能用腦子思考,這是其一;如果老師在黑板上寫演算過程,我們開始記筆記,而老師突然有一步推不下去了,阻塞住了,他在那邊思考著,而 我們呢,也不能干其他事,即使你想趁此時(shí)思考一下剛才沒聽懂的一個(gè)問題都不行,這是其二。
現(xiàn)在你應(yīng)該明白了進(jìn)程的缺陷了,而解決的辦法很簡單,我們完全可以讓聽、寫、思三個(gè)獨(dú)立的過程,并行起來,這樣很明顯可以提高聽課的效率。而實(shí)際的操作系統(tǒng)中,也同樣引入了這種類似的機(jī)制——線程。
線程的優(yōu)點(diǎn)
因?yàn)橐?strong>并發(fā),我們發(fā)明了進(jìn)程,又進(jìn)一步發(fā)明了線程。只不過進(jìn)程和線程的并發(fā)層次不同:進(jìn)程屬于在處理器這一層上提供的抽象;線程則屬于在進(jìn)程這個(gè)層 次上再提供了一層并發(fā)的抽象。如果我們進(jìn)入計(jì)算機(jī)體系結(jié)構(gòu)里,就會(huì)發(fā)現(xiàn),流水線提供的也是一種并發(fā),不過是指令級的并發(fā)。這樣,流水線、線程、進(jìn)程就從低 到高在三個(gè)層次上提供我們所迫切需要的并發(fā)!
除了提高進(jìn)程的并發(fā)度,線程還有個(gè)好處,就是可以有效地利用多處理器和多核計(jì)算機(jī)。現(xiàn)在的處理器有個(gè)趨勢就是朝著多核方向發(fā)展,在沒有線程之前多核并不能讓一個(gè)進(jìn)程的執(zhí)行速度提高,原因還是上面所有的兩點(diǎn)限制。但如果講一個(gè)進(jìn)程分解為若干個(gè)線程,則可以讓不同的線程運(yùn)行在不同的核上,從而提高了進(jìn) 程的執(zhí)行速度。
進(jìn)程和線程的區(qū)別
- 進(jìn)程是具有一定獨(dú)立功能的而程序關(guān)于某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動(dòng),進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位
- 線程是進(jìn)程的一個(gè)實(shí)體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位,線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源
- 一個(gè)線程可以創(chuàng)建和撤銷另一個(gè)線程,同一個(gè)進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行
進(jìn)程和線程的主要差別在于他們是不同的操作系統(tǒng)資源管理方式.進(jìn)程有獨(dú)立的地址空間,一個(gè)進(jìn)程崩潰后,在保護(hù)模式下不會(huì)對其他進(jìn)程產(chǎn)生影響,而線程只是一個(gè)進(jìn)程中的不同執(zhí)行路徑.線程有自己的堆棧和局部變量,但線程之間沒有單獨(dú)的地址空間,一個(gè)線程死掉就等于整個(gè)進(jìn)程死掉,所以多進(jìn)程的程序要比多線程的程序健壯,但在進(jìn)程切換時(shí),耗費(fèi)資源較大,效率要差一些.但對于一些要求同時(shí)進(jìn)行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進(jìn)程.
創(chuàng)建一個(gè)多線程程序
package thread;/** * @author Gremmie102 * @date 2022/7/21 9:15 * @purpose : 關(guān)于多線程的一些代碼,創(chuàng)建線程的第一種方法 */ class MyThread extends Thread{public void run(){//這個(gè)run方法重寫的目的,是為了明確,咱們新創(chuàng)建出來的線程要干啥活while(true){System.out.println("hello thread!");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} } public class Demo1 {public static void main(String[] args) {//創(chuàng)建一個(gè)線程//Java中創(chuàng)建線程,離不開一個(gè)關(guān)鍵的類.Thread//其中一種比較樸素的創(chuàng)建方式,是寫一個(gè)子類,繼承Thread,重寫其中的run方法//光創(chuàng)建了這個(gè)類,還不算創(chuàng)建線程,還得創(chuàng)建實(shí)例Thread t = new MyThread();//向上轉(zhuǎn)型的寫法,可寫可不寫t.start();//這才是真正開始創(chuàng)建線程//(在操作系統(tǒng)內(nèi)核中,創(chuàng)建出對應(yīng)線程的PCB,然后讓這個(gè)PCB加入到系統(tǒng)鏈表中,參與調(diào)度)//在這個(gè)代碼中,雖然先啟動(dòng)的線程,后打印的hello main//但是實(shí)際執(zhí)行的時(shí)候,看到的確是,先打印了hello main ,后打印了hello thread!//這是因?yàn)?// 1.每個(gè)線程是獨(dú)立的執(zhí)行流,//main對應(yīng)的線程是一個(gè)執(zhí)行流,MyThread是另一個(gè)執(zhí)行流.//這兩個(gè)執(zhí)行流之間是并發(fā)的執(zhí)行關(guān)系(并發(fā)+并行)//2.此時(shí)兩個(gè)線程執(zhí)行的先后順序,取決于操作系統(tǒng)調(diào)度器具體實(shí)現(xiàn)//(程序員可以把這里的調(diào)度規(guī)則,簡單得視為"隨機(jī)調(diào)度")//System.out.println("hello main");//雖然反復(fù)運(yùn)行了多次,好像結(jié)果都是一樣的,但我們的順序仍然是不可確定的//當(dāng)前看到的先打印main,大概率是受到創(chuàng)建線程自身的開銷影響的.//哪怕連續(xù)運(yùn)行1000次main在前,也不能保證1001次的時(shí)候不出現(xiàn)thread在前!//*編寫多線程代碼的時(shí)候,一定要注意到!//默認(rèn)情況下,多個(gè)線程的執(zhí)行順序,是"無序",是"隨機(jī)調(diào)度"的//進(jìn)程的退出碼為:exit code 0//操作系統(tǒng)中用進(jìn)程的退出碼來表示"進(jìn)程的運(yùn)行結(jié)果"//使用0表示進(jìn)程執(zhí)行完畢.結(jié)果正確//使用非0表示進(jìn)程執(zhí)行完畢,結(jié)果不正確//還有個(gè)別情況main還沒返回呢,進(jìn)程就崩潰,此時(shí)返回的值很可能是一個(gè)隨機(jī)值//我們可以想辦法讓這個(gè)進(jìn)程別結(jié)束得那么快,好看清楚這個(gè)進(jìn)程//我們可以搞兩個(gè)si循環(huán)while(true){System.out.println("hello main");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//運(yùn)行之后我們就可以發(fā)現(xiàn),main和thread交替打印//每一波打印機(jī)個(gè),切換到下一波是什么時(shí)候,都是不確定的,都是由調(diào)度器控制的//在JDK里提供了一個(gè)jconsole這樣的工具,可以看到Java進(jìn)程里的線程詳情.//在jdk/bin/jconsole.exe//啟動(dòng)之后先選擇我們要看哪個(gè)Java進(jìn)程//少數(shù)情況打開jconsle時(shí),可能不顯示這里的進(jìn)程列表.//這個(gè)時(shí)候退出,然后右鍵管理員運(yùn)行.//在標(biāo)簽頁中選擇線程,往下翻,在左下角部分可以查詢到當(dāng)前Java進(jìn)程中的線程信息了//剛才的死循環(huán)代碼,打印的太多太快//有的時(shí)候不希望它們打印的這么快(不方便來觀察)//我們可以用sleep()來讓線程適當(dāng)?shù)?#34;休息一下"-->指定讓線程摸一會(huì)魚,不要上cpu干活//使用Thread.sleep();的方法進(jìn)行休眠//sleep時(shí)Thread的靜態(tài)成員方法//sleep的參數(shù)是一個(gè)時(shí)間,單位是ms//計(jì)算機(jī)算的快,常用的就是ms,us,ns這結(jié)果單位//sleep(1000)就是要休眠1000ms,除非遇到一些異常打斷休眠,//所以為了防止這樣的情況發(fā)生,我們要套上try catch的殼//這里還有一個(gè)經(jīng)典面試題:談?wù)凾hread的run和start的區(qū)別//使用run,可以看到只是在打印thread,沒有打印main//直接調(diào)用run,并沒有創(chuàng)建新的線程,而是在之前的線程中,執(zhí)行了run里的內(nèi)容//使用start,則是創(chuàng)建新的線程,新的線程里面會(huì)調(diào)用run,新線程和舊線程之間是并發(fā)執(zhí)行的關(guān)系} }普通程序一般都是按照代碼執(zhí)行的順序,所以一般會(huì)卡在第一個(gè)死循環(huán)的地方,然后一直在那個(gè)地方運(yùn)行
我們寫一個(gè)簡單的main方法的時(shí)候
這里雖然我們并沒有手動(dòng)創(chuàng)建其他線程,但是Java進(jìn)程在運(yùn)行的時(shí)候,內(nèi)部也會(huì)創(chuàng)建出多個(gè)線程.
運(yùn)行這個(gè)程序,操作系統(tǒng)就會(huì)創(chuàng)建一個(gè)Java進(jìn)程,在這個(gè)Java進(jìn)程里就會(huì)有一個(gè)線程調(diào)用main方法,這個(gè)這個(gè)線程我們就稱為主線程
談到多進(jìn)程的時(shí)候,我們經(jīng)常會(huì)談到"父進(jìn)程"“子進(jìn)程”
進(jìn)程A里面創(chuàng)建了進(jìn)程B
A是B的進(jìn)程,B是A的子進(jìn)程
但是,在多線程里面,沒有"父線程""子線程"這種說法
但是仍然認(rèn)為線程之間的地位是對等的
創(chuàng)建線程
法一-創(chuàng)建Thread子類
首先我們要繼承Thread類,并且重寫其中的run方法,在這個(gè)run方法中,就描述著我們的線程要執(zhí)行的內(nèi)容.
我們創(chuàng)建一個(gè)線程實(shí)例出來,但此時(shí),該線程還沒有塞入任務(wù)鏈表中參與系統(tǒng)調(diào)度,我們還差最后一步
法二-實(shí)現(xiàn)Runnable接口
我們繼承Thread類,可以直接使用this來表示當(dāng)前線程對象的引用.
但是實(shí)現(xiàn)Runnable接口的話,this表示的就是MyRunnable的引用.需要使用Thread.currentThread()來表示當(dāng)前線程對象的引用.
法三-匿名內(nèi)部類創(chuàng)建Thread子類對象
//使用匿名內(nèi)部類創(chuàng)建Thread子類對象 Thread t1 = new Thread() {public void run(){System.out.println("使用匿名內(nèi)部類創(chuàng)建Thread子類對象");} };法四-匿名內(nèi)部類創(chuàng)建Runnable子類對象
Thread t2 = new Thread(new Runnbale() {public void run(){System.out.println("使用匿名內(nèi)部類創(chuàng)建Runnbale子類對象");} });法五-lambda表達(dá)式創(chuàng)建Runnable子類對象
Thread t2 = new Thread(() -> {System.out.println("使用匿名內(nèi)部類創(chuàng)建Thread子類對象"); });Thread的run和start的區(qū)別
package thread;/*** @author Gremmie102* @date 2022/7/21 9:15* @purpose : 關(guān)于多線程的一些代碼,創(chuàng)建線程的第一種方法*/ class MyThread extends Thread{public void run(){//這個(gè)run方法重寫的目的,是為了明確,咱們新創(chuàng)建出來的線程要干啥活while(true){System.out.println("hello thread!");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} } public class Demo1 {public static void main(String[] args) {//創(chuàng)建一個(gè)線程//Java中創(chuàng)建線程,離不開一個(gè)關(guān)鍵的類.Thread//其中一種比較樸素的創(chuàng)建方式,是寫一個(gè)子類,繼承Thread,重寫其中的run方法//光創(chuàng)建了這個(gè)類,還不算創(chuàng)建線程,還得創(chuàng)建實(shí)例Thread t = new MyThread();//向上轉(zhuǎn)型的寫法,可寫可不寫t.start();//這才是真正開始創(chuàng)建線程//(在操作系統(tǒng)內(nèi)核中,創(chuàng)建出對應(yīng)線程的PCB,然后讓這個(gè)PCB加入到系統(tǒng)鏈表中,參與調(diào)度)//在這個(gè)代碼中,雖然先啟動(dòng)的線程,后打印的hello main//但是實(shí)際執(zhí)行的時(shí)候,看到的確是,先打印了hello main ,后打印了hello thread!//這是因?yàn)?// 1.每個(gè)線程是獨(dú)立的執(zhí)行流,//main對應(yīng)的線程是一個(gè)執(zhí)行流,MyThread是另一個(gè)執(zhí)行流.//這兩個(gè)執(zhí)行流之間是并發(fā)的執(zhí)行關(guān)系(并發(fā)+并行)//2.此時(shí)兩個(gè)線程執(zhí)行的先后順序,取決于操作系統(tǒng)調(diào)度器具體實(shí)現(xiàn)//(程序員可以把這里的調(diào)度規(guī)則,簡單得視為"隨機(jī)調(diào)度")//System.out.println("hello main");//雖然反復(fù)運(yùn)行了多次,好像結(jié)果都是一樣的,但我們的順序仍然是不可確定的//當(dāng)前看到的先打印main,大概率是受到創(chuàng)建線程自身的開銷影響的.//哪怕連續(xù)運(yùn)行1000次main在前,也不能保證1001次的時(shí)候不出現(xiàn)thread在前!//*編寫多線程代碼的時(shí)候,一定要注意到!//默認(rèn)情況下,多個(gè)線程的執(zhí)行順序,是"無序",是"隨機(jī)調(diào)度"的//進(jìn)程的退出碼為:exit code 0//操作系統(tǒng)中用進(jìn)程的退出碼來表示"進(jìn)程的運(yùn)行結(jié)果"//使用0表示進(jìn)程執(zhí)行完畢.結(jié)果正確//使用非0表示進(jìn)程執(zhí)行完畢,結(jié)果不正確//還有個(gè)別情況main還沒返回呢,進(jìn)程就崩潰,此時(shí)返回的值很可能是一個(gè)隨機(jī)值//我們可以想辦法讓這個(gè)進(jìn)程別結(jié)束得那么快,好看清楚這個(gè)進(jìn)程//我們可以搞兩個(gè)si循環(huán)while(true){System.out.println("hello main");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//運(yùn)行之后我們就可以發(fā)現(xiàn),main和thread交替打印//每一波打印機(jī)個(gè),切換到下一波是什么時(shí)候,都是不確定的,都是由調(diào)度器控制的//在JDK里提供了一個(gè)jconsole這樣的工具,可以看到Java進(jìn)程里的線程詳情.//在jdk/bin/jconsole.exe//啟動(dòng)之后先選擇我們要看哪個(gè)Java進(jìn)程//少數(shù)情況打開jconsle時(shí),可能不顯示這里的進(jìn)程列表.//這個(gè)時(shí)候退出,然后右鍵管理員運(yùn)行.//在標(biāo)簽頁中選擇線程,往下翻,在左下角部分可以查詢到當(dāng)前Java進(jìn)程中的線程信息了//剛才的死循環(huán)代碼,打印的太多太快//有的時(shí)候不希望它們打印的這么快(不方便來觀察)//我們可以用sleep()來讓線程適當(dāng)?shù)?#34;休息一下"-->指定讓線程摸一會(huì)魚,不要上cpu干活//使用Thread.sleep();的方法進(jìn)行休眠//sleep時(shí)Thread的靜態(tài)成員方法//sleep的參數(shù)是一個(gè)時(shí)間,單位是ms//計(jì)算機(jī)算的快,常用的就是ms,us,ns這結(jié)果單位//sleep(1000)就是要休眠1000ms,除非遇到一些異常打斷休眠,//所以為了防止這樣的情況發(fā)生,我們要套上try catch的殼//這里還有一個(gè)經(jīng)典面試題:談?wù)凾hread的run和start的區(qū)別//使用run,可以看到只是在打印thread,沒有打印main//直接調(diào)用run,并沒有創(chuàng)建新的線程,而是在之前的線程中,執(zhí)行了run里的內(nèi)容//使用start,則是創(chuàng)建新的線程,新的線程里面會(huì)調(diào)用run,新線程和舊線程之間是并發(fā)執(zhí)行的關(guān)系} }我們再來觀察這段代碼,我們在start之后,主線程和thread(我們自己定義的線程)一起被系統(tǒng)調(diào)度執(zhí)行.那么這里兩個(gè)線程都是有個(gè)無限循環(huán)執(zhí)行的任務(wù).這里就可以很容易看出系統(tǒng)調(diào)度線程的過程
start可以看到兩個(gè)線程并發(fā)的執(zhí)行,兩組打印時(shí)交替出現(xiàn)的
如果我們直接調(diào)用run,此時(shí)并沒有創(chuàng)建新的線程,而只是在之前的線程中,執(zhí)行了run中的內(nèi)容
我們使用start,則是創(chuàng)建新的線程,新的線程里面會(huì)調(diào)用run,新線程和舊線程之間時(shí)并發(fā)執(zhí)行的關(guān)系.
那么使用多線程帶來的好處是啥呢?
hi用多線程,能夠更加充分得利用CPU多和資源
同一項(xiàng)任務(wù),我們利用多線程就可以更快的執(zhí)行完
如上代碼,我們可以通過單線程和多線程的對比發(fā)現(xiàn),運(yùn)行時(shí)間有很大的改善
那么我們注意到了這個(gè)join()的方法,這里我們在前一篇博客中有講到,join就是等待線程結(jié)束(等待線程把自己的run方法執(zhí)行完),在主線程中調(diào)用t1.join,意思就是讓main線程等待t1執(zhí)行完.這兩個(gè)join操作誰先誰后其實(shí)并不影響.
針對這里的先后順序并不影響的原因,我做一下解釋,因?yàn)槿绻窍萾1先join,t2后join.
那么主線程就要先等待t1線程執(zhí)行完,這時(shí)main線程是在阻塞狀態(tài)的,t2此時(shí)和t1并發(fā)執(zhí)行,那么當(dāng)t1執(zhí)行完之后,主線程又可以繼續(xù)執(zhí)行了,這時(shí)又運(yùn)行到了下一條t2的join,主線程又要停下來等待了,等到t2線程執(zhí)行完之后,主線程再繼續(xù).
那我們把t1和t2的順序反過來之后,也是如此,都是main線程等待t1t2都運(yùn)行結(jié)束之后再運(yùn)行,在這之前t1t2都是并發(fā)執(zhí)行的.
那么又有問題了,t2可以等待t1執(zhí)行完再去執(zhí)行嗎,可以的話代碼怎么實(shí)現(xiàn)呢?
很簡單,只要在t2的run方法中寫上t1.join就可以了.
如果我們沒有加上t1.join和t2.join,這時(shí)雖然某種意義上是并發(fā)的,但消耗的時(shí)間其實(shí)并不是單個(gè)線程的一半,比如單個(gè)線程串行執(zhí)行,消耗的時(shí)間是1300ms,那么兩個(gè)線程并發(fā)執(zhí)行確是800ms,因?yàn)檫@里
多線程的使用場景
代碼中大部分工作,都是在使用CPU進(jìn)行運(yùn)算
使用多線程,就可以更好的利用CPU多核計(jì)算資源,從而提高效率
I input 輸入
O output 輸出
讀寫硬盤,讀寫網(wǎng)卡…這些操作都算I/O
在這些場景里,就需要花很大時(shí)間來等待
像這些IO操作,都是幾乎不消耗CPU就能完成快速讀寫數(shù)據(jù)的操作
既然CPU在摸魚,就可以給他找點(diǎn)活干,也可以使用多線程,避免COU過于閑置
希望能夠幫助到你
總結(jié)
以上是生活随笔為你收集整理的JavaEE-多线程(基础篇一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 滴滴大数据面经
- 下一篇: 企业为什么要开通微信公众号?