主要總結了Java在多線程開發時遇到的一些知識點,疑惑和總結,歡迎大佬們指點交流
1.何為進程、線程
1.1進程:
簡單的說,一個進程就是一個程序執行的全部過程,是系統運行程序的基本單位。系統運行一個程序的過程既是一個進行從創建、運行到最后消亡的過程,而一個進程中可能會包含多個線程。
舉個例子,像我們電腦中運行的一個個.exe程序就是windos系統下的一個個進程,如下圖所示。
具體到我們的Java程序中,main()方法的開始執行既是一個JVM(Java虛擬機)進程的開始,而main()方法所在的線程也就是該進程下的一個線程,稱為主線程。
1.2線程
想必您已經發現,以上對進程的介紹中已多次出現線程的概念了,但是具體什么是線程呢?其實,線程和進程是很相似的概念,理解時也常常放在一起進行比較,剛才說到,進程是一個程序執行的基本單位,而線程是比進程更小的程序執行的單位。一個進程下可以存在一個或多個線程,但是多個線程在共享一個類的進程的堆和方法區資源的同時,每個線程有屬於自己的程序計數器、虛擬機棧和本地方法棧,所以系統運行程序時,線程工作或是多個線程間切換工作時的代價要遠小於進程,效率更高,從這個角度說,所以技術人員也常把線程看作是輕量級的進程來理解。
1.3進程和線程的關系
簡單來說,一個進程里可以有多個線程,線程是進程下更小的程序運行單位,多個線程共享一個進程下的堆和方法區,而每個線程有具有自己的程序計數器、虛擬機棧和本地方法棧。
進程和線程的最大不同為:各進程間一般是互相獨立的,而線程間則很可能會互相影響,進行的創建及轉換代價大,但是利於資源的管理和維護,線性的代價小,但不利於資源的統一管理和維護。
2.為什么要使用多線程
2.1並行和並發
並行:對同一個時間段,多個任務都有執行,但具體到單位時間內並不一定同時進行
並發:單位時間內,多個任務同時進行
2.2使用多線程的原因
從線程與進程性能上的區別來分析,線程作為輕量級的進程,程序執行的最小單位,在進行線程簡單切換上代價遠小於進程,而現在又是多核CPU的發展時代,滿足了多個線程可以同時運行的硬件要求,減少了線程上下文切換的代價;從時代的發展趨勢來看,現在對系統運行的並發量要求越來越高,動則成百上千萬的並發量也只有多線程能實現,因為多線程編程是高並發系統開發的基礎。
2.3多線程可能帶來的問題
內存泄漏、上下文切換、死鎖。。。
3.線程的生命周期
在Java中一個線程只能處於以下6中生命狀態中(圖片引自《Java並發編程藝術》)
一個線程的生命狀態隨着程序的運行而動態變化,Java線程的生命狀態變化如下圖所示(圖片引自《Java並發編程藝術》)
簡單來講,一個Java線程,創建后,即處於NEW狀態,調用start()方法后,線程開始准備運行,此時處於READY狀態,當處於READY狀態的線程獲得了CPU時間片后,真正達到RUNNING的運行狀態。當線程執行wait()方法后,線程進入WAITING狀態,此時的線程需要其他線程的通知才能返回剛才的運行狀態,而TIME_WAITING狀態是指等待超時的狀態,當設置的等待時間到后,原等待線程返回RUNNING狀態。當線程調用同步方法時,如果沒有獲得鎖,則進入BLOCKED狀態。最后,當線程執行完Runnable的run()方法后將會進入到最終消亡TERMINATED狀態,此時,一個線程的生命周期結束。
4.上下文切換
我們知道,對於一個CPU核來說,在同一時刻只能運行一個線程,而運行程序時的線程個數往往要大於CPU核的個數,這就涉及到CPU核在多線程間的分配問題,而CPU是怎么解決的呢?為了讓多個線程都能得到有效的運行,CPU通過為每個線程分配時間片的形式,當一個線程的時間片結束后,CPU會重新處於就緒狀態而給其他線程運行,而當前線程會保存自己的狀態以便下次再輪到該線程時能繼續運行,這樣的通過時間片的形式實現多線程切換運行的方式即為上下文切換。
5.多線程下可能遇到的線程死鎖是什么?如何避免?
5.1線程死鎖
線程死鎖是多線程在工作時遇到的阻塞狀態,因為多線程中的一個或多個線程都在等待其他線程訪問的資源被釋放,而陷入的無限期的阻塞狀態,導致被阻塞的程序永遠不會結束運行。
如下圖所示,線程1持有資源1,要訪問資源2,同時線程2持有資源2,要訪問資源1,兩個線程都在等對方釋放資源,從而進入一直阻塞的死鎖狀態。
5.2產生死鎖的條件
(1)互斥條件:某一資源在一個時刻只由一個線程所占用;
(2)請求與保持:一個線程因請求其他資源而阻塞時,以獲得的資源保持不釋放;
(3)不剝奪條件:線程已經獲得的資源在未使用結束前不能被其他線程強行使用,只有該線程使用結束后才能釋放該資源;
(4)循環等待條件:若干線程之間形成首尾相連的循環等待資源關系。
5.3如何避免死鎖
針對產生死鎖的4個條件可以聯系到,打破其中的任意一個條件即可避免線程死鎖。
(1)打破互斥條件:線程間的互斥不能被打破,因為我們就是要用鎖保證他們之間的互斥性;
(2)打破請求與保持條件:可以改為線程一次性申請所有資源;
(3)打破不剝奪條件:若某線程在占用某資源的情況下訪問其他資源而訪問失敗時,要主動釋放已經占有的資源;
(4)打破循環等待條件:線程間按照特定順序申請資源,按相反順序釋放資源。
5.4線程阻塞之sleep()和wait()方法比較
區別:
sleep()方法沒有釋放鎖,wait()方法釋放了鎖;
wait()方法被調用后,線程不會自動蘇醒,需要其他線程調用同一對象的notify()方法或notifyAll()方法去喚醒。而sleep()方法執行完后,線程會自動蘇醒;
wait()方法常用於多線程間的通信和交互,而sleep()常用於線程暫停執行。
相同點:
wait()和sleep()方法都可以實現線程的暫停功能。
6.常見疑惑之start()方法和run()方法
問:我們知道線程執行start()方法時會自動調用run()方法,那么為什么不可以直接調用run()方法呢?
解釋:先介紹一下一個線程從創建到執行的過程。首先,通過new一個Thread,我們新建了一個線程,或者說該線程處於創建狀態;然后,調用start()方法,該線性會啟動並進入准備就緒狀態,當該線程分配到CPU時間片時就可以運行了,在這里面會調用run()方法。
而start()方法並不是只實現了調用run()方法這一個操作,在期間start()會執行線程的相應的准備工作,然后自動執行run()方法里的內容,這才是真正的多線程創建和執行操作。如果直接執行run()方法的話,系統會把run()方法視為main主線程下的一個普通方法,而不會在某個線程中執行,並不是多線程工作。
7.並發編程
特點或要求:
(1)原子性:所有操作都得到執行不會受到任何因素干擾而中斷,要么都執行,要么都不執行。
(2)可見性:一個線程對共享數據進行了修改,其他線程可以看到這種修改,進而使用修改后的數據。
(3)有序性:Java編譯器會對代碼進行優化,代碼的編寫順序不一定是最終的執行順序,JVM的指令重排。
8.多線程之重要的關鍵字
8.1synchronized關鍵字
8.1.1功能
synchronized的意思為已同步化的,從單詞意思中就可以大體知道該關鍵字的作用啦。synchronized關鍵字解決多線程間訪問資源時的同步性問題,被synchronized關鍵字修飾后的方法或代碼塊能夠保證在任意時刻只有一個線程在執行。
8.1.2修飾的對象
(1)實例方法:給當前實例對象加了一把鎖,進入同步代碼前要先獲得當前實例的鎖;
(2)靜態方法:給當前類加了一把鎖,作用於該類的所有對象實例;
(3)代碼塊:給指定對象加了一把鎖,進入該代碼塊前要先獲得鎖。
8.1.3常見使用場景之單例模式
下例為雙重校驗鎖實現對象單例(線程安全)
注意:
volatile關鍵字的修飾同樣很重要,可以避免指令重排,實現多線程下的安全正常使用,具體原因為:對代碼people = new People();具體執行分為以下三部分:
(1)為people分配內存空間;
(2)初始化people;
(3)將people指向分配的內存地址。
Java虛擬機(JVM)具有指令重排的特性,也就是說執行順序可能變為1->3->2,這種情況在單線程下沒有問題,但是在多線程下會發生一個線程獲得了一個沒有實例化的實例。比如說,當線程1執行了1和3,在此時,線程2調用了getPeopleInstance()后發現people不為空,因此返回了people,但是該people還未進行初始化。而volatile關鍵字可以禁止JVM的指令重排。
8.1.4比較synchronized和ReentrantLock的異同
(1)都是可重入鎖。所謂可重入鎖就是說自己本身可以再獲取自己的內部鎖。例如,一個線程獲取了某個對象的鎖,但這個鎖還沒有釋放,當其再次想要獲得這個對象的鎖的時候還是可以的,如果不能重入鎖的話就會造成死鎖。一個線程每次獲取的鎖,鎖的計數器都會加1,只有鎖的計數器為0時才能釋放鎖。
(2)synchronized是基於JVM的,而ReentrantLock是基於API的。
(3)ReentrantLock比synchronized增加了一些高級功能。主要為:
等待可中斷:lock.lockInterruptibly()可以實現中斷等待。
可實現公平鎖:ReentrantLock可以指定是公平鎖還是非公平鎖,而synchronized只能實現非公平鎖,公平鎖就是先等待的線程也最先獲得鎖。
可實現選擇性通知:線程對象可以注冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。
8.2volatile關鍵字
目前的Java內存模型中,一個線程可以把變量保存在本地內存中,而不是直接在主存中進行讀寫操作,這樣就可能出現一個線程在主存中修改了變量值,而另一個線程還是用的它本地內存中的拷貝值,造成數據不一致,線程不安全。而volatile的作用就是告訴JVM,我修飾的這個變量是易變的,所以多線程每次使用都得到主存中進行讀寫操作。簡單來說,volatile關鍵字的作用為保證變量的可見性和禁止JVM的指令重排。
8.3synchronized和volatile關鍵字的異同
(1)性能:volatile是線程同步的輕量級實現,執行性能要優於synchronized
(2)修飾對象:volatile只能修飾變量,而synchronized可以修飾方法和代碼塊
(3)是否會阻塞:多線程訪問下,volatile不會阻塞,而synchronized可能會發生阻塞
(4)用途:volatile主要解決變量在多線程間的可見性問題,而synchronized主要解決多線程訪問時資源的同步性問題。
9.線程池
9.1什么是線程池,為什么要使用線程池
在一個應用程序中,通常需要多次使用線程,也就是說,完成一個程序執行需要多次線程的創建和銷毀過程,而創建並銷毀線程的過程會消耗寶貴的內存。為了解決重復創建和銷毀線程帶來的資源消耗問題,提出了線程池的概念,所謂線程池,通俗理解就是一個池子,里面提前准備了一些線程,具體為:提前創建一些線程,它們的集合稱為線程池,線程池在系統啟動時即創建大量空閑的線程,程序將一個任務傳給線程池,線程池就會啟動一條線程來執行這個任務,執行結束以后,該線程並不會銷毀,而是再次返回線程池中成為空閑狀態,等待執行下一個任務。通過線程池的方式可以方便的管理線程,也可以減少內存的消耗。
9.2如何創建線程池
(1)通過構造方法實現:ThreadPoolExecutor()
(2)通過Executor框架的工具類Executors來實現。
我們可以創建三種類型的ThreadPoolExecutor:
SingleThreadExecutor:該方法返回一個里面只有一個線程的線程池。只能有一個任務同時執行,若此時有多於一個任務,則多出的任務暫存在任務隊列中,等待線程空閑時一次執行任務隊列中的任務。
FixedThreadPool:該方法返回一個固定線程數量的線程池。無論有無任務執行,有多少任務執行,線程池中的線程數量都不變。當有一個新的任務要提交時,此時如果線程池中又空閑的線程,那么執行該任務;否則,擇該任務會被存放在一個任務隊列中,等待有空閑線程時再處理任務隊列中的任務。
CachedThreadPool:該方法返回一個線程數量可變的線程池。有任務提交時,若此時有空閑線程,則優先使用可復用的空閑線程。若線程池中的所有線程均已在工作,那么會創建新的線程來處理該任務。任務執行完畢后,將返回線程池進行復用。
9.3ThreadPoolExecutor類分析
ThreadPoolExecutor類提供了4個構造線程池的方法。分別是:
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit,BlockingQueue<Runnable>workQueue)
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit,BlockingQueue<Runnable>workQueue,ThreadFactory)
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit,BlockingQueue<Runnable>workQueue,
RejectedExecutionHandler)
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit,BlockingQueue<Runnable>workQueue,ThreadFactory,
RejectedExecutionHandler)
核心參數:
corePoolSize:核心線程數定義了最小可以同時進行的線程數量;
maximumPoolSize:當隊列中存放的任務數量達到隊列容量時,當前可以同時運行的線程數量變為最大線程數;
workQueue:當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到,新任務就被存放在任務隊列中。
10.多線程的實現
10.1實現Runnable接口和Callable接口比較
Runnable接口和Callable接口,都是可以編寫多線程程序的接口,都采用Thread.start()啟動線程。
從Java1.0就有Runnable接口,Java1.5時引入了Callable,目的是處理Runnable不支持的用例。Runnable接口並不會返回結果或拋出異常,但是Callable接口可以實現。
10.2execute()方法和submit()方法比較
(1)execute()方法用於不需要返回值的任務中,調用該方法無法判斷任務是否被線程池執行成功與否;
(2)submit()方法用於提交需要返回值的任務中,調用該方法后,線程池返回一個Future類型的對象,通過該對象判斷任務是否執行成功。