Java多线程开发(一)Java多线程编程简介
文章目錄
- 參考
- Java線程簡介
- Thread類構(gòu)造方法和屬性
- 常用Thread類方法
- 線程的生命周期
- 多線程編程的優(yōu)勢和風(fēng)險
- 安全性問題
- 活躍性問題
- 性能問題
參考
【Java并發(fā)系列01】Thread及ThreadGroup雜談
Java中interrupt的使用
Java 線程狀態(tài)之 RUNNABLE
鎖和監(jiān)視器之間的區(qū)別 – Java并發(fā)
Java并發(fā)編程實戰(zhàn):第一章、第十章
Java多線程編程實戰(zhàn)指南(核心篇):第一章、第二章
多線程編程對于現(xiàn)在的開發(fā)人員來說是一種很常見的技術(shù)了,特別是對于我這樣的Android程序員,“UI操作要在主線程,耗時操作要在子線程“是開始學(xué)習(xí)Android的時候就要記住的原則。但是多線程代碼要比單線程開發(fā)更復(fù)雜,還存在一些單線程編程時不會遇到的問題。這篇博客主要是介紹多線程開發(fā)的基礎(chǔ):Thread類的API 和 多線程代碼可能存在的問題。這是我們多線程開發(fā)的基礎(chǔ),下一節(jié)我會基于這篇內(nèi)容,繼續(xù)講述Java為了存在的問題而提供給我們的解決工具。
Java線程簡介
對于Java平臺來說,創(chuàng)建一個線程就是創(chuàng)建一個Thread類實例,java.lang.Thread 類是Java平臺對線程的實現(xiàn)。
Thread類構(gòu)造方法和屬性
Thread有很多的構(gòu)造方法,最后都會調(diào)到同一個初始化方法:private void init(ThreadGroup g, Runnable target, String name, long stackSize),這個方法的參數(shù)用于設(shè)置Thread的幾個屬性,它們的含義和作用方便是:
- g:ThreadGroup類型, 線程分組,用于對一組線程進(jìn)行一些批量操作,默認(rèn)值是父線程(即創(chuàng)建該線程的線程)所屬分組。
- target:Runnable類型, 實現(xiàn)線程的業(yè)務(wù)邏輯,在run()方法中被調(diào)用。
- name:String類型,作為標(biāo)識,用于打印之類的時候區(qū)分線程。默認(rèn)值格式為Thread-線程編號。
- stackSize:long類型,代表這個線程預(yù)期的堆棧大小,不指定默認(rèn)為0,表示由平臺決定。
除了上面的屬性,線程還有幾個比較重要的屬性
- priority:int類型,這個屬性可以通過setPriority(int newPriority)方法設(shè)置,這個屬性是給線程調(diào)度器的提示,表示程序希望哪個線程得到更多的運(yùn)行機(jī)會(但不意味著一定能得到)。priority的取值范圍為0~10,默認(rèn)值是父線程的priority。
- daemon:boolean類型,這個屬性可以通過setDaemon(boolean on)設(shè)置(只能在start()方法調(diào)用之前調(diào)用,即線程運(yùn)行之前調(diào)用,否則會報錯),daemon表示該線程是否為守護(hù)線程(守護(hù)線程不會阻止虛擬機(jī)的關(guān)閉),默認(rèn)值是父線程的daemon。
常用Thread類方法
- static Thread currentThread() :返回當(dāng)前線程(即當(dāng)前代碼的執(zhí)行線程)。
- static void sleep(long millis):使當(dāng)前線程釋放CPU,休眠一段時間。
- void start():啟動線程(線程啟動的過程是異步的,方法返回不代表新線程已經(jīng)開始運(yùn)行),這個方法只能被調(diào)用一次。
- void run():用于執(zhí)行任務(wù)邏輯,一般由虛擬機(jī)而非應(yīng)用程序主動調(diào)用。
- void interrupt():用于中斷線程,這個方法的效果有兩種情況:
- 如果這個線程正被阻塞,它可以迅速中斷被阻塞的線程,并拋出InterruptedException異常。
- 如果線程沒有被阻塞,它就只是設(shè)置一個中斷標(biāo)志,而不能阻止線程的繼續(xù)運(yùn)行。
- static boolean interrupted():判斷當(dāng)前線程是否已有中斷標(biāo)志并清除掉中斷標(biāo)志(即之后的代碼將不會知道線程曾經(jīng)被中斷過,除非interrupt()再次調(diào)用)。
- boolean isInterrupted():判斷當(dāng)前線程是否已有中斷標(biāo)志,這個方法不會清除中斷標(biāo)志。
線程的生命周期
一個線程在其創(chuàng)建、啟動到運(yùn)行結(jié)束的整個生命周期中會經(jīng)歷若干狀態(tài),這些狀態(tài)由枚舉類Thread.State定義,共有6種,它們分別是:
- NEW:線程創(chuàng)建之后,啟動之前處于這個狀態(tài)。因為線程只能被啟動一次,所以線程只會處于這個狀態(tài)一次。
- RUNNABLE:這個狀態(tài)可以當(dāng)做兩個子狀態(tài):READY 和RUNNING:
- READY :這個狀態(tài)的線程被稱為活躍線程,表示已經(jīng)準(zhǔn)備好由線程調(diào)度器調(diào)度轉(zhuǎn)換為RUNNING態(tài)。
- RUNNING:表示狀態(tài)正在運(yùn)行。
- BLOCKED:線程被阻塞(發(fā)起一個阻塞式I/O操作或者申請被持有的資源時),線程從 RUNNING變?yōu)檫@個狀態(tài),這個狀態(tài)下的線程不會占用CPU,當(dāng)導(dǎo)致線程阻塞的操作完成時,線程重新回到RUNNING態(tài)。
- WAITING:線程等待其他線程執(zhí)行另外特定操作而進(jìn)入該狀態(tài),同樣會釋放CPU。進(jìn)入這個狀態(tài)的方法有:Object.wait()、Thread.join()、LockSupport,park();對應(yīng)的喚醒方法是: Object.notify()、Object.notifyAll()、LockSupport.unpark(thread)。
- TIMED_WAITING:和WAITING類型,差別在這個狀態(tài)下的線程不會像WAITING態(tài)的線程一樣無限制的等下去,在指定時間內(nèi)沒有等到期待的特定操作時,這個狀態(tài)下的線程會自動轉(zhuǎn)換為RUNNING態(tài)。
- TERMINATED:已經(jīng)結(jié)束執(zhí)行的線程處于這個狀態(tài),和 NEW同理,一個線程也只會處于這個狀態(tài)一次。
我看的書上這段說的狀態(tài)劃分原則不是很清晰,綜合了一些博客和SDK里Thread類的注釋,我現(xiàn)在的理解是:
- 處于RUNNABLE態(tài)的線程區(qū)別只在于是否擁有CPU資源:即正在執(zhí)行和在等待執(zhí)行,因為CPU會根據(jù)時間分片來輪轉(zhuǎn)調(diào)度,所以這兩個子狀態(tài)的線程會隨著進(jìn)出調(diào)度隊列而在很短時間內(nèi)(毫秒級)切換子狀態(tài),從而區(qū)分的價值不大。
- BLOCKED和WAITING同樣會暫停執(zhí)行,同樣會釋放CPU。兩者的區(qū)別在于BLOCKED態(tài)的線程等待的是監(jiān)視器鎖,當(dāng)競爭鎖成功之后,這個線程就會轉(zhuǎn)換為RUNNABLE態(tài);而 WAITING態(tài)的線程是主動暫停運(yùn)行的,不涉及到鎖,并且需要其他線程調(diào)用特定方法來喚醒這個線程,否則這個線程永遠(yuǎn)不會執(zhí)行(TIMED_WAITING則會等待指定時間后醒來)。
線程整個運(yùn)行和狀態(tài)轉(zhuǎn)換規(guī)律如下圖:
多線程編程的優(yōu)勢和風(fēng)險
多線程有充分利用CPU、提高系統(tǒng)的吞吐量、更快速響應(yīng)用戶UI操作等操作等等優(yōu)勢毋庸贅言,但多線程開發(fā)相較于單線程開發(fā)也更加復(fù)雜,同時也多了很多在單線程開發(fā)時不會遇到的問題。下面我總結(jié)了多線程帶來的風(fēng)險。
安全性問題
用《Java多線程編程實戰(zhàn)指南》中的觀點,寬泛地描述多線程的安全性問題就是:
如果一個類在單線程環(huán)境下能夠運(yùn)作正常,并且在多線程環(huán)境下,在其使用方不必為其做任何改變的情況下也能運(yùn)作正常,那么我們就稱其是線程安全(Thread-safe)的,相應(yīng)地我們稱這個類具有線程安全性(Thread Safety)。反之,如果這個類在單線程環(huán)境下運(yùn)作正常而在多線程環(huán)境下則無法正常運(yùn)作,那么這個類就是非線程安全的。
為什么單線程環(huán)境下可以正常運(yùn)行的代碼放在多線程環(huán)境下運(yùn)行就會出問題呢? 對于同時運(yùn)行的多個線程來說,如果每個線程的功能和使用的資源都不涉及到其他線程,那么每個線程就是互不影響的,當(dāng)然不會存在問題,但是這種情況很少。一般而言,多個線程線程之間會存在對某些資源的共享,對應(yīng)到代碼層面上,就是共享變量的使用。由于Java內(nèi)存模型和數(shù)據(jù)緩存的影響,每個線程內(nèi)部的操作和造成的結(jié)果對外部的其他線程都不是立即可見的。由此導(dǎo)致程序出錯。出錯的原因表現(xiàn)為3個方面:原子性、可見性和有序性。
- 原子性:出于我們本意,應(yīng)該是不可分割的一個操作(這個操作要么沒有執(zhí)行,要么執(zhí)行了)被其他線程在其中插入了其他操作,導(dǎo)致狀態(tài)錯誤。
- 可見性:一個線程更新了狀態(tài)不能被另一個線程立即看到,導(dǎo)致另一個線程讀取到了舊狀態(tài)或者重復(fù)更新覆蓋掉了這次更新。
- 有序性:出于優(yōu)化的目的,編譯器和處理器會對代碼指令調(diào)整順序,這種調(diào)整的規(guī)則只保證單線程內(nèi)是正確的,而不考慮多線程。比如類a進(jìn)行操作A,C;類b進(jìn)行操作依賴于A的D,于是b通過判斷a是否已經(jīng)完成了C來決定是否進(jìn)行操作D,單線程下這個思路是正確的,但多線程下,指令重排序下a的操作A,C的順序就可能發(fā)生改變,導(dǎo)致C操作完成于A之前,以至于D操作失敗。
活躍性問題
安全性問題是保證“程序運(yùn)行時不會發(fā)生錯誤”,而活躍性則關(guān)注的是“程序會正常的結(jié)束運(yùn)行”。由于缺少某些資源或程序本身缺陷導(dǎo)致程序無法繼續(xù)下一步操作時,就是發(fā)生了活躍性問題,單線程環(huán)境下,程序進(jìn)行無限循環(huán)就是一種問題的形式,而在多線程環(huán)境下,活性問題大多由線程交互導(dǎo)致:
- 死鎖:兩個線程同時以不同的順序來獲得不同鎖,導(dǎo)致各自持有一個鎖,而請求對方持有的鎖,導(dǎo)致程序無法繼續(xù)運(yùn)行。
- 鎖死:如果有一個線程調(diào)用了wait方法,但是一直沒有其他線程調(diào)用notify方法或者是在它調(diào)用wait之前調(diào)用了notify導(dǎo)致這個線程收不到喚醒信號而一直掛起,這種情況就被稱為鎖死。
- 饑餓:修改某個線程優(yōu)先級導(dǎo)致這個線程一直得不到CPU調(diào)用。
- 活鎖:線程申請資源一直不成功,或者其他原因,導(dǎo)致線程一直重復(fù)執(zhí)行相同的操作。
性能問題
與活躍性問題密切相關(guān)的是性能問題。活躍性意味著程序會正常運(yùn)行得到結(jié)果,但是不保證效率,因為我們還希望程序的性能,即運(yùn)行的速度越快越好。多線程編程的性能過,可能由很多原因?qū)е?#xff1a;
- 頻繁的切換線程導(dǎo)致的上下文切換,導(dǎo)致CPU花費(fèi)過多時間用于線程調(diào)度。
- 多個線程共享數(shù)據(jù)為了線程安全必須使用同步機(jī)制,從而帶了額外的性能開銷。
總結(jié)
以上是生活随笔為你收集整理的Java多线程开发(一)Java多线程编程简介的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 车联网V-2X智能汽车驾驶
- 下一篇: 释迦牟尼佛和阿弥陀佛有何区别?