多用戶併發訪問是網站的基本需求,大型網站的併發用戶數會達到數萬,單臺服務器的併發用戶也會達到數百。CGI編程時代,每個用戶請求都會創建一個獨立的系統進程去處理。由於線程比進程更輕量,更少佔有系統資源,切換代價更小,所以目前主要的Web應用服務器都採用多線程的方式響應併發用戶請求,因此網站開發天然就是多線程編程。
從資源利用角度看,使用多線程的原因主要有兩個:IO阻塞與多CPU。當前線程進行IO處理的時候,會被阻塞釋放CPU以等待IO操作完成,由於IO操作(不管是磁盤IO還是網絡IO)通常都需要較長的時間,這時CPU可以調度其他的線程進行處理。前面我們提到,理想的系統Load是既沒有進程(線程)等待也沒有CPU空閒,利用多線程IO阻塞與執行交替進行,可最大限度地利用CPU資源。使用多線程的另一個原因是服務器有多個CPU,在這個連手機都有四核CPU的時代,除了最低配置的虛擬機,一般數據中心的服務器至少16核CPU,要想最大限度地使用這些CPU,必須啓動多線程。
網站的應用程序一般都被Web服務器容器管理,用戶請求的多線程也通常被Web服務器容器管理,但不管是Web容器管理的線程,還是應用程序自己創建的線程,一臺服務器上啓動多少線程合適呢?假設服務器上執行的都是相同類型任務,針對該類任務啓動的線程有個簡化的估算公式可供參考:
啓動線程數=[任務執行時間/(任務執行時間-IO等待時間)]*CPU內核數
最佳啓動線程數和CPU內核數量成正比,和IO等待時間成正比。如果任務都是CPU計算型任務,那麼線程數最多不超過CPU內核數,因爲啓動再多線程,CPU也來不及調度;相反如果是任務需要等待磁盤操作,網絡響應,那麼多啓動線程有助於提高任務併發度,提高系統吞吐能力,改善系統性能。
多線程編程一個需要注意的問題是線程安全問題,即多線程併發對某個資源進行修改,導致數據混亂。這也是缺乏經驗的網站工程師最容易犯錯的地方,而線程安全Bug又難以測試和重現,網站故障中,許多所謂偶然發生的“靈異事件”都和多線程併發問題有關。對網站而言,不管有沒有進行多線程編程,工程師寫的每一行代碼都會被多線程執行,因爲用戶請求時併發提交的,也就是說,所有的資源——對象、內存、文件、數據庫,乃至另一個線程都可能被多線程併發訪問。
編程上,解決線程安全的主要手段有如下幾點:
將對象設計爲無狀態對象:所謂無狀態對象是指對象本身不存儲狀態信息(對象無成員變量,或者成員變量也是無狀態對象),這樣多線程併發訪問的時候就不會出現狀態不一致,Java Web開發中常用的Servlet對象就設計爲無狀態對象,可以被應用服務器多相處併發嗲用處理用戶請求。而Web開發中常用的貧血模型對象都是些無狀態對象。不過從面向對象設計的角度看,無狀態對象時一種不良設計。
使用局部對象:即在方法內部創建對象,這些對象會被每個進入該方法的線程創建,除非程序有意識地將這些對象傳遞給其他線程,否則不會出現對象被多線程併發訪問的情形。
併發訪問資源時使用鎖:即多線程訪問資源的時候,通過鎖的方式使多線程併發操作轉化爲順序操作,從而避免資源被併發修改。隨着操作系統和編程語言的進步,出現各種輕量級鎖,使得運行期線程獲取鎖和釋放鎖的代價都變得更小,但是鎖導致線程同步順序執行,可能會對系統性能產生嚴重影響。
1.什麼是線程?
答:線程是指程序中的一個執行流。
2.什麼是多線程?
答:多線程是指程序中包含多個執行流。
3.進程和線程的區別?
答:進程是指正在執行的程序,線程是程序中的一個執行流。一個進程可以包含多個線程。
4.爲什麼目前主要的Web應用服務器都採用多線程的方式響應併發用戶請求,而不採用多進程?線程相比於進程有何優點?
答:線程比進程更輕量,佔用的系統資源更少,切換的代價更小。
5.什麼情況下會用到多線程?
答:響應用戶併發請求。
6.我們爲什麼要使用多線程?
答:爲了充分利用CPU資源,提高效率。
充分利用系統資源中的“充分”體現在什麼地方?
答:(1)當前線程進行IO處理(讀寫磁盤資源)時,會被阻塞釋放CPU執行權以等到IO操作完成,通常IO操作都需要較長時間,若使用多線程編程,這時CPU就可以調度其他的線程進行處理。
(2)現在服務器大多都是多核CPU,使用多線程可以最大限度地使用這些CPU。
7.一臺服務器上啓動多少線程合適呢?
答:啓動線程數=[任務執行時間/(任務執行時間-IO等待時間)]*CPU內核數
最佳啓動線程數和CPU內核數量成正比,和IO等待時間成正比。
8.多線程有幾種實現方案,分別是哪幾種?
答:三種。
繼承Thread類,重寫run()方法;
實現Runnable接口,重寫run()方法;
實現Callable接口,重寫call()方法。
9.使用多線程就一定會提高CPU的利用率(提高效率)嗎?
答:不一定。多線程如果使用得當會提高CPU的利用率,但如果使用不當的話,不僅不能提高CPU的利用率,反而會降低。因爲多線程的操作流程要比單線程的多得多,比如線程的創建和銷燬,線程之間的調度,CPU執行權的切換等等,而單線程是沒有這些問題的,所以使用多線程不一定就會提高CPU的利用率。
10.什麼是線程安全問題?
答:線程安全問題是指多個線程修改、訪問同一資源時,產生的結果不確定的情況叫做線程安全問題。
如何解決線程安全問題?
答:1.將對象設計爲無狀態對象。簡單來說,一個類中只有方法,沒有屬性,這樣的類叫無狀態類,這個類的實例對象叫無狀態對象。
2. 使用局部對象。即在方法內部創建對象,這些對象會被每個進入該方法的線程創建,除非程序有意識地將這些對象傳遞給其他線程,否則不會出現對象被多線程併發訪問的情形。
3. 加同步。加同步有兩種方法:使用sychronized關鍵字和使用Lock接口。
11.多線程實現同步的方式有哪些?
答:synchronized關鍵字;使用Lock接口。
12.同步代碼塊、同步方法和同步靜態方法的鎖對象分別是什麼?
答:同步代碼塊的鎖是任意對象
同步方法的鎖是this對象
同步靜態方法的鎖是類的字節碼對象
13. 面試題:當一個線程進入一個對象的一個synchronized()方法後,其它線程是否可進入此對象的其他方法?
答:這取決於方法本身。如果該方法是synchronized()方法(鎖對象是this對象),則不可以進入;如果該方法是非sychronized()方法,則可以進入;如果該方法是靜態方法或者靜態的synchronized()方法(鎖對象是類的字節碼對象),則可以進入。
14.同步的弊端有哪些?
答:效率低。每一次訪問都需要判斷有沒有鎖。
如果出現了同步嵌套,容易出現死鎖問題。
15.什麼是死鎖?
答:死鎖指的是兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的一種互相等待的現象。
16.用代碼寫一個死鎖。
package 死鎖;
class Test implements Runnable{ //定義一個標誌 private boolean flag; //構造函數 Test(boolean flag){ this.flag = flag; } //重寫run()方法 public void run() { if(flag){ synchronized (MyLock.locka) { System.out.println("if locka"); synchronized (MyLock.lockb) { System.out.println("if lockb"); } } }else{ synchronized (MyLock.lockb) { System.out.println("else lockb"); synchronized (MyLock.locka) { System.out.println("else locka"); } } }
}
} //定義兩個鎖 class MyLock{ static Object locka = new Object(); static Object lockb = new Object(); } //主函數 public class DeadLock { public static void main(String[] args) { Thread t1 = new Thread(new Test(true)); Thread t2 = new Thread(new Test(false)); t1.start(); t2.start(); } } |
17.如何避免死鎖問題的產生?
答:1.加鎖順序要一致。假如出現同步嵌套,都是先等待鎖A,再等待鎖B,而不是交叉等待。即一個嵌套先等待鎖A,再等待鎖B,另一個嵌套先等待鎖B,再等待鎖A。
2.要避免鎖未釋放的情況。sychronized關鍵字實現的同步會自動釋放鎖,而Lock接口需要在finally塊中調用unlock()方法釋放鎖。
18.啓動一個線程是run()還是start()?它們的區別?
答:用start()方法來啓動一個線程。
區別:
run():封裝了被線程執行的代碼,直接調用僅僅是普通方法的調用
start():啓動線程,並由JVM自動調用run()方法
19.sleep()和wait()方法的區別?
答:sleep():屬於Thread類;必須指定時間,計時時間一到,線程會自動被喚醒;不釋放鎖;可以在任何地方使用;
wait():屬於Object類;可以不指定時間,也可以指定時間需要被外界喚醒;釋放鎖;只能在同步代碼塊中使用。
20.爲什麼wait(),notify(),notifyAll()等方法都定義在Object類中?
答:因爲這些方法的調用是依賴於鎖對象的,而同步代碼塊的鎖對象是任意鎖。而Object代表任意的對象,所以,定義在這裏面。
21.線程有幾種狀態?
答:新建、就緒、運行、阻塞、死亡。其中,阻塞狀態又分三種情況:無限期等待:wait()方法,需要被外界喚醒
限期等待:sleep()方法,能夠自動喚醒。
同步等待:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入“鎖池”中,等待獲取同步鎖。
22.線程的生命週期圖
答:新建 -- 就緒 -- 運行 -- 死亡
新建 -- 就緒 -- 運行 -- 阻塞 -- 就緒 -- 運行 -- 死亡
注意:
就緒:線程具有CPU的執行資格,但沒有CPU的執行權(沒有搶到CPU執行權)。(就像一名參賽選手,有比賽的資格,但還沒有輪到他上場)
運行:有CPU執行資格,也有CPU執行權。
阻塞狀態又分三種情況:
無限期等待:wait()方法,需要被外界喚醒
限期等待:sleep()方法,能夠自動喚醒
同步等待:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入“鎖池”中,等待獲取同步鎖。
面試題:(擴展)
1.就緒和等待狀態有什麼區別?
答:阻塞狀態有CPU的執行資格,但是沒有CPU的執行權,這一狀態程序員無法控制。
等待狀態沒有CPU的執行資格,這一狀態程序員可以控制。
2.無限期等待狀態(sleep)和限期等待狀態(wait)有什麼區別?這一個問題,通常這樣問:sleep()和wait()方法的區別?
答:sleep():屬於Thread類;必須指定時間, 計時時間一到,線程會自動被喚醒;不釋放鎖;可以在任何地方使用;
wait():屬於Object類;可以不指定時間,也可以指定時間,需要被外界喚醒;釋放鎖;只能在同步代碼塊中使用。
23.終止線程的方法有哪些?
答:Thread類提供了兩個方法:stop(),suspend(),都可以用來終止線程。二者的區別是:
當調用Thread.stop()來終止線程時,會釋放鎖,可能會導致程序執行的不確定性。
當調用Thread.suspend()來終止線程時,不會釋放鎖,可稱之爲掛起線程,容易發生死鎖。
通常情況下,結束線程最好的方法是讓線程自行結束進入Dead狀態,即提供某種方式讓線程能夠自動結束run()方法的執行,例如設置一個flag標誌來控制循環是否執行,通過這種方法來讓線程離開run()方法從而終止線程。
System.exit()也可以終止線程。其實是終止虛擬機JVM。
24.什麼是守護線程?
答:java提供了兩種線程:守護線程和用戶線程。
守護線程在後臺服務於用戶線程,是用戶線程的“保姆”。
GC屬於守護線程。
25.Join()方法的作用?
答:將兩個線程合併,用於實現同步功能。
26.什麼是線程同步?
答:當多個線程訪問同一資源時,爲了防止線程安全問題的發生,需要線程同步。實現線程同步的方法一般有兩種:加sychronized關鍵字、使用Lock接口。
Sychronized關鍵字是通過同步代碼塊和同步方法來實現同步,Lock接口是通過調用lock()和unlock()方法來實現同步。
27.synchronized關鍵字和Lock接口有什麼區別?(阿里巴巴面試)
答:1.sychronized是關鍵字,Lock是接口。
2.sychronized關鍵字修飾的同步代碼塊或同步方法,鎖的獲取和釋放,並不直觀;而使用Lock接口,可以指定獲取鎖和釋放鎖的位置,比較直觀。
3.synchronized關鍵字是託管給JVM執行的,在發生異常時,會自動釋放鎖,因此不會出現死鎖;而Lock通過代碼來操作,在發生異常時,不會自動釋放鎖,可能出現死鎖,所以我們一般將Lock釋放鎖的操作放在finally塊裏。
4.代碼寫法上有區別。sychronized關鍵字實現同步,是通過同步代碼塊或同步方法實現的,在代碼塊上或方法上加sychronized關鍵字進行修飾。而Lock接口實現同步,是通過調用其lock()和unlock()方法實現的,並且lock()和unlock()方法要配合try/finally語句塊來完成。
5.相比於synchronized關鍵字,Lock接口功能更高級。比如:等待可中斷,可實現公平鎖,以及鎖可以綁定多個條件。
28.你瞭解多線程中的volatile關鍵字嗎?簡單介紹一下。
答:volatile關鍵字是Java虛擬機提供的最輕量級的同步機制。通過直接修飾共享變量,可以保證共享變量在內存中的可見性。
什麼是內存可見性?
“可見性”指的是當一條線程修改了共享變量的值時,其他線程可以立即得知這個修改。
什麼是內存不可見?
在多線程中,程序運行時,共享變量會存放在主內存中,每個線程在創建的時候都會創建一個屬於自己的工作內存,它會將主內存中的共享變量保存一份在自己的工作內存中,這樣一來就很容易造成當一個線程對共享變量進行修改時,另一個線程不知道的情況,即變量在內存中彼此不可見。 造成這個問題的原因是因爲:普通變量的值在線程間傳遞均需要通過主內存來完成,即當線程A對共享變量進行修改後,需要同步到主內存中,而線程B在獲取共享變量時,獲取到的是修改前的值,而不是修改後的值。
而當共享變量被volatile關鍵字修飾後,就能夠保證共享變量的內存可見性了。
volatile關鍵字如何保證共享變量的內存可見呢?
volatile保證共享變量內存可見性的原理是在線程每次訪問共享變量時都要進行一次刷新,線程訪問共享變量訪問的是它本身工作內存中拷貝的主內存的那一份,刷新保證了線程每次訪問之前,工作內存都要重新從主內存中拷貝一份共享變量,這就保證每次在工作內存中訪問到的共享變量都是主內存中的最新版本。
注意:線程對共享變量進行修改,要分兩步分,第一步是線程對工作內存中的共享變量副本進行修改,第二步是將工作內存中修改後的共享變量同步到主內存中,只有這兩步全部完成,才實現了線程對共享變量的修改操作。
所以,這就出現了一種情況:主內存中的共享變量volatile int i=1,假如兩個線程同時對共享變量進行修改,線程A,i =2,修改了工作內存中的共享變量,還沒有同步到主內存中,這時,線程B搶到了CPU的執行權,也開始對共享變量進行修改,修改之前先刷新,獲取到的最新的i=1,修改爲i=3,並同步到主內存中,主內存i=3,這時,CPU執行權被線程A搶走了,主內存i=2,最後發現i=2。有人就有疑問了?我覺着i應該爲3呀,爲什麼是2?這不就是共享變量並沒有內存可見嘛!你看,我線程A先修改的i = 2,我修改完之後,線程B刷新獲取到的值還是i=1,並不是i=2,這不是沒有實現內存可見嗎?我的回答是這樣:我前面說過了,對共享變量的修改分兩步,第一步是修改工作內存中的共享變量副本,第二步是將修改結果同步到主內存中去。上面的情況,當線程B搶到CPU執行權,對共享變量進行修改時,線程A還沒有完成對共享變量的修改,所以這是主內存中i=1就是最新版本,線程B刷新獲取到i=1並沒有違背內存可見性。
29.你瞭解多線程中的synchronized關鍵字嗎?簡單介紹一下。
答:當多個線程操作同一資源的時候,容易產生線程安全問題,解決線程安全問題的方法是同步,synchronized關鍵字是同步的一種方式。它可以加在代碼塊上構成同步代碼塊,也可以加在方法上構成同步方法,用synchronized關鍵字解決同步問題需要鎖對象,同步代碼塊的鎖對象是任意對象,同步方法的鎖對象是this對象,同步靜態方法的鎖對象是類的字節碼對象。
30.volatile相較於synchronized的區別是什麼?
答:1.volatile可以直接修飾變量,而synchronize不可以直接修飾變量
2.volatile只能使用在變量級別(通過直接修飾變量實現),而synchronized既可以變量級別,也可以使用在方法和類級別(通過同步代碼塊和同步方法實現)。
3.volatile能變量的內存可見性、有序性,不能保證變量的原子性;而sychronized既可以保證變量的內存可見性、有序性,也可以保證變量的原子性。
31.爲什麼volatile關鍵字不能保證變量的原子性?原因何在?
答:原因是volatile關鍵字修飾的變量,如果當前值與該變量以前的值相關,則volatile關鍵字不起作用。例如,有一個共享變量volatile int i = 1;在多個線程中執行了一下操作:i++或i = i+1,這時,volatile不能保證i值的內存可見性。
32.被volatile關鍵字修飾的變量具備哪些特點?
答:可見性和有序性。
33.對於volatile型變量的特殊規則
答:當一個變量定義爲volatile之後,它將具備兩種特性:
第一是保證此變量對所有線程的可見性。這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得到的。而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內存來完成,例如,線程A修改一個普通變量的值,然後向主內存進行回寫,另外一條線程B在線程A回寫完成了之後再從主內存進行讀取操作,新變量值纔會對線程B可見。
volatile變量在各個線程的工作內存中不存在一致性問題(在各個線程的工作內存中,volatile變量也可以存在不一致的情況,但由於每次使用之前都要先刷新,執行引擎看不到不一致的情況,因此可以認爲不存在一致性問題),但是Java裏面的運算並非原子操作,導致volatile變量的運算在併發下一樣是不安全。
由於volatile變量只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖(使用sychronized或java.util.concurrent中的原子類)來保證原子性。
£運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
£變量不需要與其他的狀態變量共同參與不變約束。
第二個語義是禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。因爲在一個線程的方法執行過程中無法感知到這點,這也就是Java內存中描述的所謂的“線程內表現爲串行的語義”。
上面的描述仍然不太容易理解,我們舉一個具體的例子來說明:
public class Single{ //構造函數私有化 private Single(){}
//定義靜態成員變量 private static Single s = null;
//對外提供公共的訪問方法 public static Single getInstance(){ if(s == null){ synchronized(Single.class){ if(s == null){ s = new Single(); } } } return s; } } |
就如上面所示,這個代碼看起來很完美,理由如下:
•如果檢查第一個singleton不爲null,則不需要執行下面的加鎖動作,極大提高了程序的性能。
•如果第一個singleton爲null,即使有多個線程同一時間判斷,但是由於synchronized的存在,只會有一個線程能夠創建對象。
•當第一個獲取鎖的線程創建完成singleton對象後,其他的在第二次判斷singleton一定不會爲null,則直接返回已經創建好的singleton對象。
通過上面的分析,DCL看起來確實是非常完美,但是可以明確地告訴你,這個錯誤的。上面的邏輯確實是沒有問題,分析也對,但是就是有問題,那麼問題出在哪裏呢?
在回答這個問題之前,我們先來複習一下創建對象過程,實例化一個對象要分爲三個步驟:
1. 分配內存空間;
2. 初始化對象;
3. 將內存空間的地址賦值給對應的引用;
但是由於重排序的緣故,步驟2、3可能會發生重排序,其過程如下:
1. 分配內存空間;
2. 將內存空間的地址賦值給對應的引用;
3. 初始化對象;
如果2、3發生了重排序就會導致第二個判斷會出錯,singleton != null,但是它其實僅僅只是一個地址而已,此時對象還沒有被初始化,所以return的singleton對象是一個沒有被初始化的對象。假如有兩個線程A、B,線程A先執行,但是發生重排序了,所以線程A返回的singleton對象是一個沒有被初始化的對象,僅僅只是一個地址而已。當線程B訪問的時候,singleton != null,但是線程B拿到的僅僅只是一個地址而已,是一個沒有被初始化的對象。
通過上面的闡述,我們可以判斷DCL的錯誤根源在於步驟4:
Singleton = new Singleton();
知道問題根源所在,那麼怎麼解決呢?有兩個解決辦法:
1. 不允許初始化階段步驟2、3發生重排序。
2. 允許初始化階段步驟2、3發生重排序,但是不允許其他線程“看到”這個重排序。
這裏我們講第一種解決方案,很簡單:將變量singleton聲明爲volatile即可:
public class Single{ //構造函數私有化 private Single(){}
//定義靜態成員變量 private volatile static Single s = null;
//對外提供公共的訪問方法 public static Single getInstance(){ if(s == null){ synchronized(Single.class){ if(s == null){ s = new Single(); } } }
return s; } } |
當singleton聲明爲volatile後,步驟2、步驟3就不會被重排序了,也就可以解決上面那問題了。
33.原子性、可見性與有序性
答:Java內存模型是圍繞着在併發過程中如果處理原子性、可見性和有序性這3個特徵來建立的,我們逐個來看一下哪些操作實現了這3個特性。
原子性(Atomicity)。由Java內存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認爲基本數據類型的訪問讀寫是具備原子性的(例外就是long和double的非原子性協定,讀者只要知道這件事情就可以了,無須太過在意這些幾乎不會發生的例外情況)。
如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java內存模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱士地使用這兩個操作,這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。
可見性(Visibility)。可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。上文在講解volatile變量的時候我們已詳細討論過這一點。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。因此,可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。
除了volatile之外,Java還有兩個關鍵字能實現可見性,即sychronized和final。同步塊的可見性是由“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那在其他線程中就能看見final字段的值。
有序性(Ordering)。Java內存模型的有序性在前面講解volatile時也詳細地討論過了,Java程序中天然的有序性可以總結爲一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行的語義”(Within-Thread As-If-Serial Semantics),後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。
Java語言提供了volatile和sychronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而sychronized則是由“一個變量在同一個時刻只允許一條線程對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
總結:
原子性:原子即不可分割的意思,要麼操作全部成功,要麼全部失敗。舉個例子,給int i=5賦值,只有兩種情況,要麼賦值成功,要麼賦值不成功。所以說基本數據類型的訪問讀寫是具備原子性的。
可見性:可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。
有序性:有序性可以總結爲一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行的語義”,後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。
什麼是指令重排序現象?
答:舉個例子。創建對象的過程大體可分爲三步:
1.分配內存空間
2.初始化對象
3.將內存空間的地址賦值給對應的引用
但由於指令重排序的緣故,步驟2、3可能會發生重排序,其過程如下:
1.分配內存空間
2.將內存空間的地址賦值給對應的引用
3.初始化對象
線程池(掌握的不好)
34.什麼是線程池?
答:線程池可以理解爲是一個可以容納多個線程的容器,裏面的線程可以處於等待狀態,避免了頻繁創建和銷燬線程造成的系統資源浪費。
35.爲什麼要使用線程池?
答:在java開發中,如果每一個請求的到來都需要創建一個新線程,那麼對於系統資源的浪費是非常嚴重的,我們需要想一個辦法來儘可能減少創建和銷燬線程的次數。於是線程池應運而生了。線程池就是一個可以容納多個線程的容器,裏面的線程可以處於等待狀態,用的時候直接從線程池中拿,不用的時候再放回到線程池中去,避免了頻繁創建和銷燬線程造成的系統資源浪費。
補充:
我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?在Java中可以通過線程池來達到這樣的效果。
使用線程池的好處?
答:1.頻繁創建和銷燬線程會降低系統執行效率。線程池可以緩存線程,當線程池中的線程執行完一個任務時,並不會直接銷燬,而是會處於等待狀態,當有新任務到來的時候,等待的線程可以付勇。
2.避免因爲線程併發數量過多而導致的系統資源阻塞。線程能共享系統資源,如果同時執行的線程數量過多,就有可能導致系統資源不足而產生阻塞的情況,運用線程池能有效的控制線程最大併發數,避免以上問題。
3.對線程進行一些簡單的管理。比如延時執行、定時循環執行的策略等。
36.線程池的創建一般是通過ThreadPoolExecutor類來進行的,ThreadPoolExecutor類的構造函數有多個,對線程池的配置,就是對ThreadPoolExecutor構造函數的參數的配置,主要的參數有哪些呢?
答:核心線程池大小(corePoolSize)、最大線程池大小(maximumPoolSize)、workQueue(任務隊列)、保持存活時間(keepAliveTime)、unit(keepAliveTime的單位,秒、分鐘啊)等。
corePoolSize:核心線程池的大小。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把新到達的任務放到任務隊列中進行緩存。
線程池新建線程的時候,如果當前線程總數小於corePoolSize,則新建的是核心線程,如果超過corePoolSize,則新建的是非核心線程。 核心線程默認情況下會一直存活在線程池中,即使這個核心線程啥也不幹。如果設置allowCoreThreadTimeOut=true,閒置狀態的核心線程也會被銷燬。
maximumPoolSize:最大線程池大小。最大線程數=核心線程數+非核心線程數。這個參數表示線程池中最多能創建多少個線程。
workQueue:任務隊列。當所有核心線程都在幹活時,新添加的任務會被添加到這個隊列中等待處理,如果隊列滿了,則新建非核心線程執行任務。說白了,我線程池中的核心線程數量有限,可是任務源源不斷的往線程池中加,線程不夠呀,所以把多餘的任務放在任務隊列中進行等待。當任務隊列也滿了的時候,線程池就會創建非核心線程了,當核心線程和非核心線程總數超過maximumPoolSize線程池允許創建的最大線程數時,就會拋異常了,我這個線程池承受不住啦!
keepAliveTime:線程保持存活時間。一個非核心線程,如果不幹活(閒置狀態)的時長超過這個參數所設定的時長,就會被銷燬掉。如果設置allowCoreThreadTimeOut=true,則會作用於核心線程。
當一個任務被添加進線程池時,經過哪些步驟?
答:1.線程數量未達到corePoolSize,則新建一個線程(核心線程)執行任務。
2.線程數量達到了corePoolSize,則將任務添加到任務隊列進行緩存。
3.當任務隊列已滿,新建線程(非核心線程)執行任務。
4.當任務隊列已滿,並且總線程數又達到了maximumPoolSize,就會拋出異常。
另一種說法:
線程池按以下行爲執行任務
1. 當線程數小於核心線程數時,創建線程。
2. 當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
3. 當線程數大於等於核心線程數,且任務隊列已滿
若線程數小於最大線程數,創建線程
若線程數等於最大線程數,拋出異常,拒絕任務
37.線程池的種類有哪些?
答:Java通過Executors提供了四種線程池,這四種線程池都是直接或間接配置ThreadPoolExecutor的參數實現的,它們分別是:固定大小線程池(FixedThreadPool)、可緩存線程池(CacheThreadPool)、單線程池(SingleThreadPool )、定時器線程池(newScheduledThreadPool)。