1.Java中多線程相關
1.1 線程的建立方式
【方法1】html
【方法2】android中實現多線程,通常使用此方法爲主;java
【調用】【說明】認識:start方法調用以後並不是直接能夠開啓多線程的執行,具體線程開始執行的時間由操做系統決定;mysql
【共同點】都須要使用到Thread類產生線程;而後start開啓線程;android
【不一樣點】程序員
【1】繼承Thread類是單繼承,所以使用runnable類彌補缺陷,更靈活;面試
【2】繼承Thread類就必須產生多個相應的線程;spring
而使用runnable接口,則須要實現runnable接口的實例,而後用此實例開啓多個線程就實現了多線程共享資源了;sql
【runnable的優點】數據庫
【1】避免Thread類單繼承帶來的侷限性;編程
【2】加強程序的健壯性,代碼能夠被多個線程共享,同時代碼和數據是獨立的;
【3】適合多個線程對同一個資源的共享;
1.2 Start和run方法的區別
【start方法】使用start方法以後是真正的開啓了多線程,沒必要等待run方法中的方法體中的內容的執行完畢就能夠直接調用start方法下面的方法;
【run方法】只是一個普通的方法,其中是方法的執行體,方法的執行仍是順序執行,沒有開啓多線程,沒有併發;
【多線程原理】:至關於玩遊戲機,只有一個遊戲機(cpu),但是有不少人要玩,因而,start是排隊!等CPU選中你就是輪到你,你就run(),當CPU的運行的時間片執行完,這個線程就繼續排隊,等待下一次的run()。
調用start()後,線程會被放到等待隊列,等待CPU調度,並不必定要立刻開始執行,只是將這個線程置於可動行狀態。而後經過JVM,線程Thread會調用run()方法,執行本線程的線程體。
先調用start後調用run,這麼麻煩,爲了避免直接調用run?就是爲了實現多線程的優勢,沒這個start不行。
1.start()方法來啓動線程,真正實現了多線程運行。
這時無需等待run方法體代碼執行完畢,能夠直接繼續執行下面的代碼;
經過調用Thread類的start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並無運行。
而後經過此Thread類調用方法run()來完成其運行操做的, 這裏方法run()稱爲線程體,它包含了要執行的這個線程的內容, Run方法運行結束, 此線程終止。
而後CPU再調度其它線程。
2.run()方法看成普通方法的方式調用。
程序仍是要順序執行,要等待run方法體執行完畢後,纔可繼續執行下面的代碼;
程序中只有主線程——這一個線程, 其程序執行路徑仍是隻有一條, 這樣就沒有達到寫線程的目的。
記住:多線程就是分時利用CPU,宏觀上讓全部線程一塊兒執行 ,也叫併發
1 public class Main {
2
3 public static void main(String[] args) {
4 Thread t1 = new Thread(new T1());
5 Thread t2 = new Thread(new T2());
6 t1.start();
7 t2.start();
8 }
9
10 }
11
12 class T1 implements Runnable {
13 public void run() {
14 try {
15 for(int i=0;i<10;i++){
16 System.out.println(i);
17 Thread.sleep(100); //模擬耗時任務
18 }
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 }
23 }
24
25 class T2 implements Runnable {
26 public void run() {
27 try {
28 for(int i=0;i>-10;i--){
29 System.out.println(i);
30 Thread.sleep(100); //模擬耗時任務
31 }
32 } catch (InterruptedException e) {
33 e.printStackTrace();
34 }
35 }
36 }
1 public class Main {
2
3 public static void main(String[] args) {
4 Thread t1 = new Thread(new T1());
5 Thread t2 = new Thread(new T2());
6 t1.run();
7 t2.run();
8 }
9
10 }
可見兩線程實際是順序執行的;
2.線程間的通訊
2.1 synchronized對象鎖
2.2 synchronized實現進程間通訊
2.3 synchronized/volatile關鍵字
首先咱們要先意識到有這樣的現象,編譯器爲了加快程序運行的速度,對一些變量的寫操做會先在寄存器或者是CPU緩存上進行,最後才寫入內存.
而在這個過程當中,變量的新值對其餘線程是不可見的.
當對volatile標記的變量進行修改時,會將其餘緩存中存儲的修改前的變量清除,而後從新讀取。這裏從哪讀取我並不明確,
通常來講應該是先在進行修改的緩存A中修改成新值,而後通知其餘緩存清除掉此變量,
當其餘緩存B中的線程讀取此變量時,會向總線發送消息,這時存儲新值的緩存A獲取到消息,
將新值穿給B。最後將新值寫入內存。當變量須要更新時都是此步驟,volatile的做用是被其修飾的變量,每次更新時,都會刷新上述步驟。
【原文地址】值得一看:https://my.oschina.net/tantexian/blog/808032
volatile保證了可見性,可是並不保證原子性!!!
1.volatile關鍵字的兩層語義
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:
1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
2)禁止進行指令重排序。
volatile的可見性,即任什麼時候刻只要有任何線程修改了volatile變量的值,其餘線程總能獲取到該最新值。
什麼是指令重排序?有兩個層面:
- 在虛擬機層面,爲了儘量減小內存操做速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照本身的一些規則(這規則後面再敘述)將程序編寫順序打亂——即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以儘量充分地利用CPU。
拿上面的例子來講:假如不是a=1的操做,而是a=new byte[1024*1024](分配1M空間),那麼它會運行地很慢,此時CPU是等待其執行結束呢,仍是先執行下面那句flag=true呢?顯然,先執行flag=true能夠提早使用CPU,加快總體效率,固然這樣的前提是不會產生錯誤(什麼樣的錯誤後面再說)。雖然這裏有兩種狀況:後面的代碼先於前面的代碼開始執行;前面的代碼先開始執行,但當效率較慢的時候,後面的代碼開始執行並先於前面的代碼執行結束。無論誰先開始,總以後面的代碼在一些狀況下存在先結束的可能。
- 在硬件層面,CPU會將接收到的一批指令按照其規則重排序,一樣是基於CPU速度比緩存速度快的緣由,和上一點的目的相似,只是硬件處理的話,每次只能在接收到的有限指令範圍內重排序,而虛擬機能夠在更大層面、更多指令範圍內重排序。硬件的重排序機制參見《從JVM併發看CPU內存指令重排序(Memory Reordering)》
2.4 volatile與synchronized 區別
一、volatile本質是在告訴jvm當前變量在寄存器中的值是不肯定的,須要從主存中讀取,
synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住.
volatile的第一條語義是保證線程間變量的可見性,簡單地說就是當線程A對變量X進行了修改後,在線程A後面執行的其餘線程能看到變量X的變更,更詳細地說是要符合如下兩個規則:
線程對變量進行修改以後,要馬上回寫到主內存。
線程對變量讀取的時候,要從主內存中讀,而不是緩存。
二、volatile僅能使用在變量級別;
synchronized則能夠使用在變量、方法、和類級別的
三、volatile僅能實現變量的修改可見性,而synchronized則能夠保證變量的修改可見性和原子性.
四、volatile不會形成線程的阻塞,而synchronized可能會形成線程的阻塞.
五、volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化
六、使用volatile而不是synchronized的惟一安全的狀況是:類中只有一個可變的域。
七、當一個域的值依賴於它以前的值時,volatile就沒法工做了,如n=n+1,n++等。
若是某個域的值受到其餘域的值的限制,那麼volatile也沒法工做,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。
2.5 synchronized和lock的區別
【lock方法】只要指明起始位置和終止位置;通常就重入鎖;
須要lcok和unlock對鎖的開啓和關閉;
通常會在finally中對鎖進行unlock,防止死鎖;
|
類別 |
synchronized |
Lock |
1 |
存在層次 |
Java的關鍵字,在jvm層面上,性能低下 |
是一個類,代碼本身定義的 |
2 |
鎖的釋放 |
一、以獲取鎖的線程執行完同步代碼,釋放鎖 二、線程執行發生異常,jvm會讓線程釋放鎖 |
在finally中必須釋放鎖,否則容易形成線程死鎖 |
3 |
鎖的獲取 |
假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 |
分狀況而定,Lock有多個鎖獲取的方式,具體下面會說道,大體就是能夠嘗試得到鎖,線程能夠不用一直等待 |
4 |
鎖狀態 |
沒法判斷 |
能夠判斷 |
5 |
鎖類型 |
可重入 不可中斷 非公平 |
可重入 可判斷 可公平(二者皆可) |
6 |
性能 |
少許同步 |
大量同步 |
7 |
cpu鎖的性質 |
悲觀鎖 (阻塞等待) |
樂觀鎖 |
悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。
樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,能夠使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,
像數據庫若是提供相似於write_condition機制的其實都是提供的樂觀鎖。
兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下,即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是常常產生衝突,上層應用會不斷的進行retry,這樣反卻是下降了性能,因此這種狀況下用悲觀鎖就比較合適。
技術點:
一、線程與進程:
在開始以前先把進程與線程進行區分一下,一個程序最少須要一個進程,而一個進程最少須要一個線程。
關係是線程–>進程–>程序的大體組成結構。
因此
【線程】是程序執行流的最小單位,
【進程】是系統進行資源分配和調度的一個獨立單位。
如下咱們全部討論的都是創建在線程基礎之上。
二、Thread的幾個重要方法:
咱們先了解一下Thread的幾個重要方法。
a、start()方法,調用該方法開始執行該線程;
b、stop()方法,調用該方法強制結束該線程執行;
d、sleep()方法,調用該方法該線程進入等待。
e、run()方法,調用該方法直接執行線程的run()方法,可是線程調用start()方法時也會運行run()方法,區別就是一個是由線程調度運行run()方法,一個是直接調用了線程中的run()方法!!
看到這裏,可能有些人就會問啦,那wait()和notify()呢?要注意,其實wait()與notify()方法是Object的方法,不是Thread的方法!!
同時,wait()與notify()會配合使用,分別表示線程掛起和線程恢復。
補充兩個重要的方法:yield()和join()
f.yield方法 暫停當前正在執行的線程對象。 yield()方法是中止當前線程,讓同等優先權的線程或更高優先級的線程有執行的機會。若是沒有的話,那麼yield()方法將不會起做用,而且由可執行狀態後立刻又被執行。 g.join方法是用於在某一個線程的執行過程當中調用另外一個線程執行,等到被調用的線程執行結束後,再繼續執行當前線程。如:t.join();//主要用於等待t線程運行結束,若無此句,main則會執行完畢,致使結果不可預測。
這裏還有一個很常見的問題,順帶提一下:
【wait()與sleep()的區別】簡單來講wait()會釋放對象鎖而sleep()不會釋放對象鎖。
【1】對於sleep()方法,咱們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。
【2】sleep()方法致使了程序暫停執行指定的時間,讓出cpu該其餘線程,可是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態
在調用sleep()方法的過程當中,線程不會釋放對象鎖。
而當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備
獲取對象鎖進入運行狀態。
什麼意思呢?
舉個列子說明:
5
6 /**
7 * java中的sleep()和wait()的區別
10 */
11 public class TestD {
12
13 public static void main(String[] args) {
14 new Thread(new Thread1()).start();
15 try {
16 Thread.sleep(5000);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20 new Thread(new Thread2()).start();
21 }
22
23 private static class Thread1 implements Runnable{
24 @Override
25 public void run(){
26 synchronized (TestD.class) {
27 System.out.println("enter thread1...");
28 System.out.println("thread1 is waiting...");
29 try {
30 //調用wait()方法,線程會放棄對象鎖,進入等待此對象的等待鎖定池
31 TestD.class.wait();
32 } catch (Exception e) {
33 e.printStackTrace();
34 }
35 System.out.println("thread1 is going on ....");
36 System.out.println("thread1 is over!!!");
37 }
38 }
39 }
40
41 private static class Thread2 implements Runnable{
42 @Override
43 public void run(){
44 synchronized (TestD.class) {
45 System.out.println("enter thread2....");
46 System.out.println("thread2 is sleep....");
47 //只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備獲取對象鎖進入運行狀態。
48 TestD.class.notify();
49 //==================
50 //區別
51 //若是咱們把代碼:TestD.class.notify();給註釋掉,即TestD.class調用了wait()方法,可是沒有調用notify()
52 //方法,則線程永遠處於掛起狀態。
53 try {
54 //sleep()方法致使了程序暫停執行指定的時間,讓出cpu該其餘線程,
55 //可是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態。
56 //在調用sleep()方法的過程當中,線程不會釋放對象鎖。
57 Thread.sleep(5000);
58 } catch (Exception e) {
59 e.printStackTrace();
60 }
61 System.out.println("thread2 is going on....");
62 System.out.println("thread2 is over!!!");
63 }
64 }
65 }
66 }
運效果
enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
thread1 is going on ....
thread1 is over!!!
若是註釋掉代碼:
運行效果:
enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
且程序一直處於掛起狀態。
===============================================================================================
Java中wait和sleep方法的區別
1、二者的區別
【1】這兩個方法來自不一樣的類分別是Thread和Object
【2】最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其餘線程能夠使用同步控制塊或者方法(鎖代碼塊和方法鎖)。
wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep能夠在任何地方使用(使用範圍)
【3】sleep必須捕獲異常,而wait,notify和notifyAll不須要捕獲異常
sleep方法屬於Thread類中方法,表示讓一個線程進入睡眠狀態,等待必定的時間以後,自動醒來進入到可運行狀態,不會立刻進入運行狀態,由於線程調度機制恢復線程的運行也須要時間,一個線程對象調用了sleep方法以後,
並不會釋放他所持有的全部對象鎖,因此也就不會影響其餘進程對象的運行。但在sleep的過程當中過程當中有可能被其餘對象調用它的interrupt(),產生InterruptedException異常,
若是你的程序不捕獲這個異常,線程就會異常終止,進入TERMINATED狀態,若是你的程序捕獲了這個異常,那麼程序就會繼續執行catch語句塊(可能還有finally語句塊)以及之後的代碼。
注意sleep()方法是一個靜態方法,也就是說他只對當前對象有效,經過t.sleep()讓t對象進入sleep,這樣的作法是錯誤的,它只會是使當前線程被sleep 而不是t線程
wait屬於Object的成員方法,一旦一個對象調用了wait方法,必需要採用notify()和notifyAll()方法喚醒該進程;若是線程擁有某個或某些對象的同步鎖,那麼在調用了wait()後,這個線程就會釋放它持有的全部同步資源,
而不限於這個被調用了wait()方法的對象。wait()方法也一樣會在wait的過程當中有可能被其餘對象調用interrupt()方法而產生
若是線程A但願當即結束線程B,則能夠對線程B對應的Thread實例調用interrupt方法。若是此刻線程B正在wait/sleep/join,則線程B會馬上拋出InterruptedException,在catch() {} 中直接return便可安全地結束線程。
須要注意的是,InterruptedException是線程本身從內部拋出的,並非interrupt()方法拋出的。對某一線程調用interrupt()時,若是該線程正在執行普通的代碼,那麼該線程根本就不會拋出InterruptedException。可是,一旦該線程進入到wait()/sleep()/join()後,就會馬上拋出InterruptedException。
waite()和notify()由於會對對象的「鎖標誌」進行操做,因此它們必須在synchronized函數或synchronized block中進行調用。若是在non-synchronized函數或non-synchronizedblock中進行調用,雖然能編譯經過,但在運行時會發生illegalMonitorStateException的異常。
說一下爲何使用wait()方法時,通常是須要while循環而不是if?
複製代碼
while(!執行條件) {
wait();
}
....
if(!執行條件) {
wait();
}
....
複製代碼
while會一直執行循環,直到條件知足,執行條件纔會繼續往下執行。if只會執行一次判斷條件,不知足就會等待。這樣就會出現問題。
咱們知道用notify() 和notifyAll()能夠喚醒線程,通常咱們經常使用的是notifyAll(),由於notify(),只會隨機喚醒一個睡眠線程,並不必定是咱們想要喚醒的線程。
若是使用的是notifyAll(),喚醒全部的線程,那你怎麼知道他想喚醒的是某個正在等待的wait()線程呢,若是用while()方法,就會再次判斷條件是否是成立,知足執行條件了,就會接着執行,
而if會直接喚醒wait()方法,繼續往下執行,根本無論這個notifyAll()是否是想喚醒的是本身仍是別人,可能此時if的條件根本沒成立。
舉個例子:
while去水果店買蘋果,沒有了,而後while就和水果店老闆說,有水果的時候通知我,我先回去了。if也去水果店買蘋果,沒有了,而後if就和水果店老闆說,有水果的時候通知我,我先回去了。
過一段時間,水果店老闆發短信告訴while和if,有水果了,while去一看,水果店只是進了香蕉,並非蘋果,因此不是想要的水果,因而回去繼續等水果店老闆通知,
而if根本就不看是否是本身想要的蘋果,直接就叫老闆送10斤水果過來,這樣會致使你獲得錯誤的結果。
三、線程狀態:
線程總共有5大狀態,經過上面第二個知識點的介紹,理解起來就簡單了。
-
新建狀態:新建線程對象,並無調用start()方法以前
-
就緒狀態:調用start()方法以後線程就進入就緒狀態,可是並非說只要調用start()方法線程就立刻變爲當前線程,在變爲當前線程以前都是爲就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態。
-
運行狀態:線程被設置爲當前線程,開始執行run()方法。就是線程進入運行狀態
-
阻塞狀態:線程被暫停,好比說調用sleep()方法後線程就進入阻塞狀態
-
死亡狀態:線程執行結束
四、鎖類型
2.6 Lock詳細介紹與Demo
【原文地址】https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1
如下是Lock接口的源碼,筆者修剪以後的結果:
1 public interface Lock {
2
3 /**
4 * Acquires the lock.
5 */
6 void lock();
7
8 /**
9 * Acquires the lock unless the current thread is
10 * {@linkplain Thread#interrupt interrupted}.
11 */
12 void lockInterruptibly() throws InterruptedException;
13
14 /**
15 * Acquires the lock only if it is free at the time of invocation.
16 */
17 boolean tryLock();
18
19 /**
20 * Acquires the lock if it is free within the given waiting time and the
21 * current thread has not been {@linkplain Thread#interrupt interrupted}.
22 */
23 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
24
25 /**
26 * Releases the lock.
27 */
28 void unlock();
29
30 }
31 從Lock接口中咱們能夠看到主要有個方法,這些方法的功能從註釋中能夠看出:
32 lock():獲取鎖,若是鎖被暫用則一直等待
33
34 unlock():釋放鎖
35
36 tryLock(): 注意返回類型是boolean,若是獲取鎖的時候鎖被佔用就返回false,不然返回true
37
38 tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間
39
40 lockInterruptibly():用該鎖的得到方式,若是線程在獲取鎖的階段進入了等待,那麼能夠中斷此線程,先去作別的事
41
42 經過 以上的解釋,大體能夠解釋在上個部分中「鎖類型(lockInterruptibly())」,「鎖狀態(tryLock())」等問題,還有就是前面子所獲取的過程我所寫的「大體就是能夠嘗試得到鎖,線程能夠不會一直等待」用了「能夠」的緣由。
43
44 下面是Lock通常使用的例子,注意ReentrantLock是Lock接口的實現。
45 【lock()】
48 package com.brickworkers;
49
50 import java.util.concurrent.locks.Lock;
51 import java.util.concurrent.locks.ReentrantLock;
52
53 public class LockTest {
54 private Lock lock = new ReentrantLock();
55
56 //須要參與同步的方法
57 private void method(Thread thread){
58 lock.lock();
59 try {
60 System.out.println("線程名"+thread.getName() + "得到了鎖");
61 }catch(Exception e){
62 e.printStackTrace();
63 } finally {
64 System.out.println("線程名"+thread.getName() + "釋放了鎖");
65 lock.unlock();
66 }
67 }
68
69 public static void main(String[] args) {
70 LockTest lockTest = new LockTest();
71
72 //線程1
73 Thread t1 = new Thread(new Runnable() {
74
75 @Override
76 public void run() {
77 lockTest.method(Thread.currentThread());
78 }
79 }, "t1");
80
81 Thread t2 = new Thread(new Runnable() {
82
83 @Override
84 public void run() {
85 lockTest.method(Thread.currentThread());
86 }
87 }, "t2");
88
89 t1.start();
90 t2.start();
91 }
92 }
93 //執行狀況:線程名t1得到了鎖
94 // 線程名t1釋放了鎖
95 // 線程名t2得到了鎖
96 // 線程名t2釋放了鎖
97 【tryLock()】:
99 package com.brickworkers;
100
101 import java.util.concurrent.locks.Lock;
102 import java.util.concurrent.locks.ReentrantLock;
103
104 public class LockTest {
105 private Lock lock = new ReentrantLock();
106
107 //須要參與同步的方法
108 private void method(Thread thread){
109 /* lock.lock();
110 try {
111 System.out.println("線程名"+thread.getName() + "得到了鎖");
112 }catch(Exception e){
113 e.printStackTrace();
114 } finally {
115 System.out.println("線程名"+thread.getName() + "釋放了鎖");
116 lock.unlock();
117 }*/
118
119
120 if(lock.tryLock()){
121 try {
122 System.out.println("線程名"+thread.getName() + "得到了鎖");
123 }catch(Exception e){
124 e.printStackTrace();
125 } finally {
126 System.out.println("線程名"+thread.getName() + "釋放了鎖");
127 lock.unlock();
128 }
129 }else{
130 System.out.println("我是"+Thread.currentThread().getName()+"有人佔着鎖,我就不要啦");
131 }
132 }
133
134 public static void main(String[] args) {
135 LockTest lockTest = new LockTest();
136
137 //線程1
138 Thread t1 = new Thread(new Runnable() {
139
140 @Override
141 public void run() {
142 lockTest.method(Thread.currentThread());
143 }
144 }, "t1");
145
146 Thread t2 = new Thread(new Runnable() {
147
148 @Override
149 public void run() {
150 lockTest.method(Thread.currentThread());
151 }
152 }, "t2");
153
154 t1.start();
155 t2.start();
156 }
157 }
158
159 //執行結果: 線程名t2得到了鎖
160 // 我是t1有人佔着鎖,我就不要啦
161 // 線程名t2釋放了鎖
162 看到這裏相信你們也都會使用如何使用Lock了吧,關於tryLock(long time, TimeUnit unit)和lockInterruptibly()再也不贅述。
前者主要存在一個等待時間,在測試代碼中寫入一個等待時間,後者主要是等待中斷,會拋出一箇中斷異常,經常使用度不高,喜歡探究能夠本身深刻研究。
163
164 前面比較重提到「公平鎖」,在這裏能夠提一下ReentrantLock對於平衡鎖的定義,在源碼中有這麼兩段:
165
166 /**
167 * Sync object for non-fair locks
168 */
169 static final class NonfairSync extends Sync {
170 private static final long serialVersionUID = 7316153563782823691L;
171
172 /**
173 * Performs lock. Try immediate barge, backing up to normal
174 * acquire on failure.
175 */
176 final void lock() {
177 if (compareAndSetState(0, 1))
178 setExclusiveOwnerThread(Thread.currentThread());
179 else
180 acquire(1);
181 }
182
183 protected final boolean tryAcquire(int acquires) {
184 return nonfairTryAcquire(acquires);
185 }
186 }
187
188 /**
189 * Sync object for fair locks
190 */
191 static final class FairSync extends Sync {
192 private static final long serialVersionUID = -3000897897090466540L;
193
194 final void lock() {
195 acquire(1);
196 }
197
198 /**
199 * Fair version of tryAcquire. Don't grant access unless
200 * recursive call or no waiters or is first.
201 */
202 protected final boolean tryAcquire(int acquires) {
203 final Thread current = Thread.currentThread();
204 int c = getState();
205 if (c == 0) {
206 if (!hasQueuedPredecessors() &&
207 compareAndSetState(0, acquires)) {
208 setExclusiveOwnerThread(current);
209 return true;
210 }
211 }
212 else if (current == getExclusiveOwnerThread()) {
213 int nextc = c + acquires;
214 if (nextc < 0)
215 throw new Error("Maximum lock count exceeded");
216 setState(nextc);
217 return true;
218 }
219 return false;
220 }
221 }
222 從以上源碼能夠看出在Lock中能夠本身控制鎖是否公平,並且,默認的是非公平鎖,如下是ReentrantLock的構造函數:
223
224 public ReentrantLock() {
225 sync = new NonfairSync();//默認非公平鎖
226 }
尾記錄:
延伸學習:對於LOCK底層的實現,你們能夠參考:
點擊Lock底層介紹博客
兩種同步方式性能測試,你們能夠參考:
點擊查看兩種同步方式性能測試博客
博主18年3月新增:
回來看本身博客。發現東西闡述的不夠完整。這裏在作補充,由於這篇博客訪問較大,因此爲了避免誤導你們,儘可能介紹給你們正確的表述:
一、兩種鎖的底層實現方式:
synchronized:咱們知道java是用字節碼指令來控制程序(這裏不包括熱點代碼編譯成機器碼)。
在字節指令中,存在有synchronized所包含的代碼塊,那麼會造成2段流程的執行。
咱們點擊查看SyncDemo.java的源碼SyncDemo.class,能夠看到以下:
如上就是這段代碼段字節碼指令,沒你想的那麼難吧。言歸正傳,咱們能夠清晰段看到,其實synchronized映射成字節碼指令就是增長來兩個指令:monitorenter和monitorexit。當一條線程進行執行的遇到monitorenter指令的時候,它會去嘗試得到鎖,若是得到鎖那麼鎖計數+1(爲何會加一呢,由於它是一個可重入鎖,因此須要用這個鎖計數判斷鎖的狀況),若是沒有得到鎖,那麼阻塞。當它遇到monitorexit的時候,鎖計數器-1,當計數器爲0,那麼就釋放鎖。
那麼有的朋友看到這裏就疑惑了,那圖上有2個monitorexit呀?立刻回答這個問題:上面我之前寫的文章也有表述過,synchronized鎖釋放有兩種機制,一種就是執行完釋放;另一種就是發送異常,虛擬機釋放。圖中第二個monitorexit就是發生異常時執行的流程,這就是我開頭說的「會有2個流程存在「。並且,從圖中咱們也能夠看到在第13行,有一個goto指令,也就是說若是正常運行結束會跳轉到19行執行。
這下,你對synchronized是否是瞭解的很清晰了呢。接下來咱們再聊一聊Lock。
Lock:Lock實現和synchronized不同,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,因此它每次吃東西前都把本身關起來。而Lock呢底層實際上是CAS樂觀鎖的體現,它無所謂,別人搶了它吃的,它從新去拿吃的就好啦,因此它很樂觀。具體底層怎麼實現,博主不在細述,有機會的話,我會對concurrent包下面的機制好好和你們說說,若是面試問起,你就說底層主要靠volatile和CAS操做實現的。
如今,纔是我真正想在這篇博文後面加的,我要說的是:儘量去使用synchronized而不要去使用LOCK
什麼概念呢?我和你們打個比方:你叫jdk,你生了一個孩子叫synchronized,後來呢,你領養了一個孩子叫LOCK。起初,LOCK剛來到新家的時候,它很乖,很懂事,各個方面都表現的比synchronized好。你很開心,可是你心裏深處又有一點淡淡的憂傷,你不但願你本身親生的孩子居然還不如一個領養的孩子乖巧。這個時候,你對親生的孩子教育更加深入了,你想證實,你的親生孩子synchronized並不會比領養的孩子LOCK差。(博主只是打個比方)
那如何教育呢?
在jdk1.6~jdk1.7的時候,也就是synchronized1六、7歲的時候,你做爲爸爸,你給他優化了,具體優化在哪裏呢:
一、線程自旋和適應性自旋
咱們知道,java’線程實際上是映射在內核之上的,線程的掛起和恢復會極大的影響開銷。而且jdk官方人員發現,不少線程在等待鎖的時候,在很短的一段時間就得到了鎖,因此它們在線程等待的時候,並不須要把線程掛起,而是讓他無目的的循環,通常設置10次。這樣就避免了線程切換的開銷,極大的提高了性能。
而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。他能夠根據它前面線程的自旋狀況,從而調整它的自旋,甚至是不通過自旋而直接掛起。
二、鎖消除
什麼叫鎖消除呢?就是把沒必要要的同步在編譯階段進行移除。
那麼有的小夥伴又迷糊了,我本身寫的代碼我會不知道這裏要不要加鎖?我加了鎖就是表示這邊會有同步呀?
並非這樣,這裏所說的鎖消除並不必定指代是你寫的代碼的鎖消除,我打一個比方:
在jdk1.5之前,咱們的String字符串拼接操做其實底層是StringBuffer來實現的(這個你們能夠用我前面介紹的方法,寫一個簡單的demo,而後查看class文件中的字節碼指令就清楚了),而在jdk1.5以後,那麼是用StringBuilder來拼接的。咱們考慮前面的狀況,好比以下代碼:
String str1="qwe"; String str2="asd"; String str3=str1+str2;
底層實現會變成這樣:
StringBuffer sb = new StringBuffer(); sb.append("qwe"); sb.append("asd");
咱們知道,StringBuffer是一個線程安全的類,也就是說兩個append方法都會同步,經過指針逃逸分析(就是變量不會外泄),咱們發如今這段代碼並不存在線程安全問題,這個時候就會把這個同步鎖消除。
三、鎖粗化
在用synchronized的時候,咱們都講究爲了不大開銷,儘可能同步代碼塊要小。那麼爲何還要加粗呢?
咱們繼續以上面的字符串拼接爲例,咱們知道在這一段代碼中,每個append都須要同步一次,那麼我能夠把鎖粗化到第一個append和最後一個append(這裏不要去糾結前面的鎖消除,我只是打個比方)
四、輕量級鎖
五、偏向鎖
2.8 wait/notify/notifyall
先說兩個概念:鎖池和等待池
【鎖池】:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),
因爲這些線程在進入對象的synchronized方法以前必須先得到該對象的鎖的擁有權,可是該對象的鎖目前正被線程A擁有,因此這些線程就進入了該對象的鎖池中。
【等待池】假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,線程A進入到了該對象的等待池中
Reference:
java中的鎖池和等待池
而後再來講notify和notifyAll的區別
也就是說,調用了notify後只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的全部線程移動到鎖池中,等待鎖競爭
- 優先級高的線程競爭到對象鎖的機率大,倘若某線程沒有競爭到該對象鎖,它還會留在鎖池中,惟有線程再次調用 wait()方法,它纔會從新回到等待池中。
而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。
Reference:
線程間協做:wait、notify、notifyAll
綜上,【所謂喚醒線程】另外一種解釋能夠說是將線程由等待池移動到鎖池,notifyAll調用後,會將所有線程由等待池移到鎖池,而後參與鎖的競爭,競爭成功則繼續執行,若是不成功則留在鎖池等待鎖被釋放後再次參與競爭。
而notify只會喚醒一個線程。
有了這些理論基礎,後面的notify可能會致使死鎖,而notifyAll則不會的例子也就好解釋了
3.線程池
3.1 線程池的優勢
合理利用線程池可以帶來三個好處。
第一:下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。
第二:提升響應速度。當任務到達時,任務能夠不須要等到線程建立就能當即執行。
第三:提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。
可是要作到合理的利用線程池,必須對其原理了如指掌
3.2 線程池的使用
線程池的建立
咱們能夠經過ThreadPoolExecutor來建立一個線程池。6個參數。
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
建立一個線程池須要輸入幾個參數:
- corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會建立一個線程來執行任務,即便其餘空閒的基本線程可以執行新任務也會建立線程,等到須要執行的任務數大於線程池基本大小時就再也不建立。
若是調用了線程池的prestartAllCoreThreads方法,線程池會提早建立並啓動全部基本線程。
- runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。 能夠選擇如下幾個阻塞隊列。
- ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
- LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
- PriorityBlockingQueue:一個具備優先級的無限阻塞隊列。
- maximumPoolSize(線程池最大大小):線程池容許建立的最大線程數。若是隊列滿了,而且已建立的線程數小於最大線程數,則線程池會再建立新的線程執行任務。值得注意的是若是使用了無界的任務隊列這個參數就沒什麼效果。
- ThreadFactory:用於設置建立線程的工廠,能夠經過線程工廠給每一個建立出來的線程設置更有意義的名字。
- RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採起一種策略處理提交的新任務。這個策略默認狀況下是AbortPolicy,表示沒法處理新任務時拋出異常。如下是JDK1.5提供的四種策略。
- AbortPolicy:直接拋出異常。
- CallerRunsPolicy:只用調用者所在線程來運行任務。
- DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
- DiscardPolicy:不處理,丟棄掉。
- 固然也能夠根據應用場景須要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務。
- keepAliveTime(線程活動保持時間):線程池的工做線程空閒後,保持存活的時間。因此若是任務不少,而且每一個任務執行的時間比較短,能夠調大這個時間,提升線程的利用率。
- TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
向線程池提交任務
咱們能夠使用execute提交的任務,可是execute方法沒有返回值,因此沒法判斷任務是否被線程池執行成功。經過如下代碼可知execute方法輸入的任務是一個Runnable類的實例。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
咱們也能夠使用submit 方法來提交任務,它會返回一個future,那麼咱們能夠經過這個future來判斷任務是否執行成功,經過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後當即返回,這時有可能任務沒有執行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 處理中斷異常
} catch (ExecutionException e) {
// 處理沒法執行任務異常
} finally {
// 關閉線程池
executor.shutdown();
}
線程池的關閉
咱們能夠經過調用線程池的shutdown或shutdownNow方法來關閉線程池,它們的原理是遍歷線程池中的工做線程,而後逐個調用線程的interrupt方法來中斷線程,因此沒法響應中斷的任務可能永遠沒法終止。
可是它們存在必定的區別,shutdownNow首先將線程池的狀態設置成STOP,而後嘗試中止全部的正在執行或暫停任務的線程,並返回等待執行任務的列表,而shutdown只是將線程池的狀態設置成SHUTDOWN狀態,而後中斷全部沒有正在執行任務的線程。
只要調用了這兩個關閉方法的其中一個,isShutdown方法就會返回true。當全部的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至於咱們應該調用哪種方法來關閉線程池,應該由提交到線程池的任務特性決定,一般調用shutdown來關閉線程池,若是任務不必定要執行完,則能夠調用shutdownNow。
3.3. 線程池的分析
流程分析:線程池的主要工做流程以下:
源碼分析。上面的流程分析讓咱們很直觀的瞭解了線程池的工做原理,讓咱們再經過源代碼來看看是如何實現的。線程池執行任務的方法以下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//若是線程數小於基本線程數,則建立線程並執行當前任務
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如線程數大於等於基本線程數或線程建立失敗,則將當前任務放到工做隊列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//若是線程池不處於運行中或任務沒法放入隊列,而且當前線程數量小於最大容許的線程數量,
則建立一個線程執行任務。
else if (!addIfUnderMaximumPoolSize(command))
//拋出RejectedExecutionException異常
reject(command); // is shutdown or saturated
}
}
工做線程。線程池建立線程時,會將線程封裝成工做線程Worker,Worker在執行完任務後,還會無限循環獲取工做隊列裏的任務來執行。咱們能夠從Worker的run方法裏看到這點:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//若是線程數小於基本線程數,則建立線程並執行當前任務
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如線程數大於等於基本線程數或線程建立失敗,則將當前任務放到工做隊列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//若是線程池不處於運行中或任務沒法放入隊列,而且當前線程數量小於最大容許的線程數量,
則建立一個線程執行任務。
else if (!addIfUnderMaximumPoolSize(command))
//拋出RejectedExecutionException異常
reject(command); // is shutdown or saturated
}
}
3.5. 合理的配置線程池
要想合理的配置線程池,就必須首先分析任務特性,能夠從如下幾個角度來進行分析:
- 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
- 任務的優先級:高 中 低
- 任務的執行時間:長 中 短。
- 任務的依賴性:是否依賴其餘系統資源,如數據庫鏈接。
任務性質不一樣的任務能夠用不一樣規模的線程池分開處理。CPU密集型任務配置儘量小的線程,如配置Ncpu+1個線程的線程池。IO密集型任務則因爲線程並非一直在執行任務,則配置儘量多的線程,如2*Ncpu。混合型的任務,若是能夠拆分,則將其拆分紅一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,若是這兩個任務執行時間相差太大,則不必進行分解。咱們能夠經過Runtime.getRuntime().availableProcessors()方法得到當前設備的CPU個數。
優先級不一樣的任務能夠使用優先級隊列PriorityBlockingQueue來處理。它可讓優先級高的任務先獲得執行,須要注意的是若是一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。
執行時間不一樣的任務能夠交給不一樣規模的線程池來處理,或者也能夠使用優先級隊列,讓執行時間短的任務先執行。
依賴數據庫鏈接池的任務,由於線程提交SQL後須要等待數據庫返回結果,若是等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。
建議使用有界隊列,有界隊列能增長系統的穩定性和預警能力,能夠根據須要設大一點,好比幾千。有一次咱們組使用的後臺任務線程池的隊列和線程池全滿了,不斷的拋出拋棄任務的異常,經過排查發現是數據庫出現了問題,致使執行SQL變得很是緩慢,由於後臺任務線程池裏的任務全是須要向數據庫查詢和插入數據的,因此致使線程池裏的工做線程所有阻塞住,任務積壓在線程池裏。若是當時咱們設置成無界隊列,線程池的隊列就會愈來愈多,有可能會撐滿內存,致使整個系統不可用,而不僅是後臺任務出現問題。固然咱們的系統全部的任務是用的單獨的服務器部署的,而咱們使用不一樣規模的線程池跑不一樣類型的任務,可是出現這樣問題時也會影響到其餘任務。
3.6 線程池的監控
經過線程池提供的參數進行監控。線程池裏有一些屬性在監控線程池的時候能夠使用
- taskCount:線程池須要執行的任務數量。
- completedTaskCount:線程池在運行過程當中已完成的任務數量。小於或等於taskCount。
- largestPoolSize:線程池曾經建立過的最大線程數量。經過這個數據能夠知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。
- getPoolSize:線程池的線程數量。若是線程池不銷燬的話,池裏的線程不會自動銷燬,因此這個大小隻增不+ getActiveCount:獲取活動的線程數。
經過擴展線程池進行監控。經過繼承線程池並重寫線程池的beforeExecute,afterExecute和terminated方法,咱們能夠在任務執行前,執行後和線程池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }
【博客參考】線程池
在Java中能夠經過線程池來達到這樣的效果。今天咱們就來詳細講解一下Java的線程池,首先咱們從最核心的ThreadPoolExecutor類中的方法講起,而後再講述它的實現原理,接着給出了它的使用示例,最後討論了一下如何合理配置線程池的大小。
如下是本文的目錄大綱:
一.Java中的ThreadPoolExecutor類
二.深刻剖析線程池實現原理
三.使用示例
四.如何合理配置線程池的大小
一.Java中的ThreadPoolExecutor類
java.uitl.concurrent.ThreadPoolExecutor類是線程池中最核心的一個類,所以若是要透徹地瞭解Java中的線程池,必須先了解這個類。下面咱們來看一下ThreadPoolExecutor類的具體實現源碼。
在ThreadPoolExecutor類中提供了四個構造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
從上面的代碼能夠得知,ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,事實上,經過觀察每一個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工做。
下面解釋下一下構造器中各個參數的含義:
【corePoolSize】:核心池的大小,這個參數跟後面講述的線程池的實現原理有很是大的關係。在建立了線程池後,默認狀況下,線程池中並無任何線程,而是等待有任務到來才建立線程去執行任務,
除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就能夠看出,是預建立線程的意思,即在沒有任務到來以前就建立corePoolSize個線程或者一個線程。
默認狀況下,在建立了線程池後,線程池中的線程數爲0,當有任務來以後,就會建立一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
【maximumPoolSize】:線程池最大線程數,這個參數也是一個很是重要的參數,它表示在線程池中最多能建立多少個線程;
【keepAliveTime】:表示線程沒有任務執行時最多保持多久時間會終止。默認狀況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起做用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,若是一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。可是若是調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起做用,直到線程池中的線程數爲0;
【unit】:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,通常來講,這裏的阻塞隊列有如下幾種選擇:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用較少,通常使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。
threadFactory:線程工廠,主要用來建立線程;
handler:表示當拒絕處理任務時的策略,有如下四種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,可是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,而後從新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
具體參數的配置與線程池的關係將在下一節講述。
從上面給出的ThreadPoolExecutor類的代碼能夠知道,ThreadPoolExecutor繼承了AbstractExecutorService,咱們來看一下AbstractExecutorService的實現:
public abstract class AbstractExecutorService implements ExecutorService {
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
public Future<?> submit(Runnable task) {};
public <T> Future<T> submit(Runnable task, T result) { };
public <T> Future<T> submit(Callable<T> task) { };
private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
};
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
};
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
};
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
};
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
};
}
AbstractExecutorService是一個抽象類,它實現了ExecutorService接口。
咱們接着看ExecutorService接口的實現:
public interface ExecutorService extends Executor {
void shutdown();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
而ExecutorService又是繼承了Executor接口,咱們看一下Executor接口的實現:
public interface Executor {
void execute(Runnable command);
}
到這裏,你們應該明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor幾個之間的關係了。
Executor是一個頂層接口,在它裏面只聲明瞭一個方法execute(Runnable),返回值爲void,參數爲Runnable類型,從字面意思能夠理解,就是用來執行傳進去的任務的;
而後ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的全部方法;
而後ThreadPoolExecutor繼承了類AbstractExecutorService。
在ThreadPoolExecutor類中有幾個很是重要的方法:
execute()
submit()
shutdown()
shutdownNow()
【execute()】方法其實是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,經過這個方法能夠向線程池提交一個任務,交由線程池去執行。
【submit()】方法是在ExecutorService中聲明的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中並無對其進行重寫,這個方法也是用來向線程池提交任務的,可是它和execute()方法不一樣,它可以返回任務執行的結果,去看submit()方法的實現,會發現它實際上仍是調用的execute()方法,只不過它利用了Future來獲取任務執行結果(Future相關內容將在下一篇講述)。
【shutdown()和shutdownNow()】是用來關閉線程池的。
還有不少其餘的方法:
好比:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等獲取與線程池相關屬性的方法,有興趣的朋友能夠自行查閱API。
二.深刻剖析線程池實現原理
在上一節咱們從宏觀上介紹了ThreadPoolExecutor,下面咱們來深刻解析一下線程池的具體實現原理,將從下面幾個方面講解:
1.線程池狀態
2.任務的執行
3.線程池中的線程初始化
4.任務緩存隊列及排隊策略
5.任務拒絕策略
6.線程池的關閉
7.線程池容量的動態調整
1.線程池狀態
在ThreadPoolExecutor中定義了一個volatile變量,另外定義了幾個static final變量表示線程池的各個狀態:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runState表示當前線程池的狀態,它是一個volatile變量用來保證線程之間的可見性;
下面的幾個static final變量表示runState可能的幾個取值。
當建立線程池後,初始時,線程池處於RUNNING狀態;
若是調用了shutdown()方法,則線程池處於SHUTDOWN狀態,此時線程池不可以接受新的任務,它會等待全部任務執行完畢;
若是調用了shutdownNow()方法,則線程池處於STOP狀態,此時線程池不能接受新的任務,而且會去嘗試終止正在執行的任務;
當線程池處於SHUTDOWN或STOP狀態,而且全部工做線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態。
2.任務的執行
在瞭解將任務提交給線程池到任務執行完畢整個過程以前,咱們先來看一下ThreadPoolExecutor類中其餘的一些比較重要成員變量:
private final BlockingQueue<Runnable> workQueue; //任務緩存隊列,用來存放等待執行的任務
private final ReentrantLock mainLock = new ReentrantLock(); //線程池的主要狀態鎖,對線程池狀態(好比線程池大小
//、runState等)的改變都要使用這個鎖
private final HashSet<Worker> workers = new HashSet<Worker>(); //用來存放工做集
private volatile long keepAliveTime; //線程存貨時間
private volatile boolean allowCoreThreadTimeOut; //是否容許爲核心線程設置存活時間
private volatile int corePoolSize; //核心池的大小(即線程池中的線程數目大於這個參數時,提交的任務會被放進任務緩存隊列)
private volatile int maximumPoolSize; //線程池最大能容忍的線程數
private volatile int poolSize; //線程池中當前的線程數
private volatile RejectedExecutionHandler handler; //任務拒絕策略
private volatile ThreadFactory threadFactory; //線程工廠,用來建立線程
private int largestPoolSize; //用來記錄線程池中曾經出現過的最大線程數
private long completedTaskCount; //用來記錄已經執行完畢的任務個數
每一個變量的做用都已經標明出來了,這裏要重點解釋一下corePoolSize、maximumPoolSize、largestPoolSize三個變量。
corePoolSize在不少地方被翻譯成核心池大小,其實個人理解這個就是線程池的大小。舉個簡單的例子:
假若有一個工廠,工廠裏面有10個工人,每一個工人同時只能作一件任務。
所以只要當10個工人中有工人是空閒的,來了任務就分配給空閒的工人作;
當10個工人都有任務在作時,若是還來了任務,就把任務進行排隊等待;
若是說新任務數目增加的速度遠遠大於工人作任務的速度,那麼此時工廠主管可能會想補救措施,好比從新招4個臨時工人進來;
而後就將任務也分配給這4個臨時工人作;
若是說着14個工人作任務的速度仍是不夠,此時工廠主管可能就要考慮再也不接收新的任務或者拋棄前面的一些任務了。
當這14個工人當中有人空閒時,而新任務增加的速度又比較緩慢,工廠主管可能就考慮辭掉4個臨時工了,只保持原來的10個工人,畢竟請額外的工人是要花錢的。
這個例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是說corePoolSize就是線程池大小,maximumPoolSize在我看來是線程池的一種補救措施,即任務量忽然過大時的一種補救措施。
不過爲了方便理解,在本文後面仍是將corePoolSize翻譯成核心池大小。
largestPoolSize只是一個用來起記錄做用的變量,用來記錄線程池中曾經有過的最大線程數目,跟線程池的容量沒有任何關係。
下面咱們進入正題,看一下任務從提交到最終執行完畢經歷了哪些過程。
在ThreadPoolExecutor類中,最核心的任務提交方法是execute()方法,雖然經過submit也能夠提交任務,可是實際上submit方法裏面最終調用的仍是execute()方法,因此咱們只須要研究execute()方法的實現原理便可:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
上面的代碼可能看起來不是那麼容易理解,下面咱們一句一句解釋:
首先,判斷提交的任務command是否爲null,如果null,則拋出空指針異常;
接着是這句,這句要好好理解一下:
1
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))
因爲是或條件運算符,因此先計算前半部分的值,若是線程池中當前線程數不小於核心池大小,那麼就會直接進入下面的if語句塊了。
若是線程池中當前線程數小於核心池大小,則接着執行後半部分,也就是執行
1
addIfUnderCorePoolSize(command)
若是執行完addIfUnderCorePoolSize這個方法返回false,則繼續執行下面的if語句塊,不然整個方法就直接執行完畢了。
若是執行完addIfUnderCorePoolSize這個方法返回false,而後接着判斷:
1
if (runState == RUNNING && workQueue.offer(command))
若是當前線程池處於RUNNING狀態,則將任務放入任務緩存隊列;若是當前線程池不處於RUNNING狀態或者任務放入緩存隊列失敗,則執行:
1
addIfUnderMaximumPoolSize(command)
若是執行addIfUnderMaximumPoolSize方法失敗,則執行reject()方法進行任務拒絕處理。
回到前面:
1
if (runState == RUNNING && workQueue.offer(command))
這句的執行,若是說當前線程池處於RUNNING狀態且將任務放入任務緩存隊列成功,則繼續進行判斷:
1
if (runState != RUNNING || poolSize == 0)
這句判斷是爲了防止在將此任務添加進任務緩存隊列的同時其餘線程忽然調用shutdown或者shutdownNow方法關閉了線程池的一種應急措施。若是是這樣就執行:
1
ensureQueuedTaskHandled(command)
進行應急處理,從名字能夠看出是保證 添加到任務緩存隊列中的任務獲得處理。
咱們接着看2個關鍵方法的實現:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask); //建立線程去執行firstTask任務
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
這個是addIfUnderCorePoolSize方法的具體實現,從名字能夠看出它的意圖就是當低於核心吃大小時執行的方法。下面看其具體實現,首先獲取到鎖,由於這地方涉及到線程池狀態的變化,先經過if語句判斷當前線程池中的線程數目是否小於核心池大小,有朋友也許會有疑問:前面在execute()方法中不是已經判斷過了嗎,只有線程池當前線程數目小於核心池大小纔會執行addIfUnderCorePoolSize方法的,爲什麼這地方還要繼續判斷?緣由很簡單,前面的判斷過程當中並無加鎖,所以可能在execute方法判斷的時候poolSize小於corePoolSize,而判斷完以後,在其餘線程中又向線程池提交了任務,就可能致使poolSize不小於corePoolSize了,因此須要在這個地方繼續判斷。而後接着判斷線程池的狀態是否爲RUNNING,緣由也很簡單,由於有可能在其餘線程中調用了shutdown或者shutdownNow方法。而後就是執行
1
t = addThread(firstTask);
這個方法也很是關鍵,傳進去的參數爲提交的任務,返回值爲Thread類型。而後接着在下面判斷t是否爲空,爲空則代表建立線程失敗(即poolSize>=corePoolSize或者runState不等於RUNNING),不然調用t.start()方法啓動線程。
咱們來看一下addThread方法的實現:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w); //建立一個線程,執行任務
if (t != null) {
w.thread = t; //將建立的線程的引用賦值爲w的成員變量
workers.add(w);
int nt = ++poolSize; //當前線程數加1
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
在addThread方法中,首先用提交的任務建立了一個Worker對象,而後調用線程工廠threadFactory建立了一個新的線程t,而後將線程t的引用賦值給了Worker對象的成員變量thread,接着經過workers.add(w)將Worker對象添加到工做集當中。
下面咱們看一下Worker類的實現:
private final class Worker implements Runnable {
private final ReentrantLock runLock = new ReentrantLock();
private Runnable firstTask;
volatile long completedTasks;
Thread thread;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
}
boolean isActive() {
return runLock.isLocked();
}
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) {
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
void interruptNow() {
thread.interrupt();
}
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
if (runState < STOP &&
Thread.interrupted() &&
runState >= STOP)
boolean ran = false;
beforeExecute(thread, task); //beforeExecute方法是ThreadPoolExecutor類的一個方法,沒有具體實現,用戶能夠根據
//本身須要重載這個方法和後面的afterExecute方法來進行一些統計信息,好比某個任務的執行時間等
try {
task.run();
ran = true;
afterExecute(task, null);
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
} finally {
runLock.unlock();
}
}
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this); //當任務隊列中沒有任務時,進行清理工做
}
}
}
它實際上實現了Runnable接口,所以上面的Thread t = threadFactory.newThread(w);效果跟下面這句的效果基本同樣:
1
Thread t = new Thread(w);
至關於傳進去了一個Runnable任務,在線程t中執行這個Runnable。
既然Worker實現了Runnable接口,那麼天然最核心的方法即是run()方法了:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
從run方法的實現能夠看出,它首先執行的是經過構造器傳進來的任務firstTask,在調用runTask()執行完firstTask以後,在while循環裏面不斷經過getTask()去取新的任務來執行,那麼去哪裏取呢?天然是從任務緩存隊列裏面去取,getTask是ThreadPoolExecutor類中的方法,並非Worker類中的方法,下面是getTask方法的實現:
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //若是線程數大於核心池大小或者容許爲核心池線程設置空閒時間,
//則經過poll取任務,若等待必定的時間取不到任務,則返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) { //若是沒取到任務,即r爲null,則判斷當前的worker是否能夠退出
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers(); //中斷處於空閒狀態的worker
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
在getTask中,先判斷當前線程池狀態,若是runState大於SHUTDOWN(即爲STOP或者TERMINATED),則直接返回null。
若是runState爲SHUTDOWN或者RUNNING,則從任務緩存隊列取任務。
若是當前線程池的線程數大於核心池大小corePoolSize或者容許爲核心池中的線程設置空閒存活時間,則調用poll(time,timeUnit)來取任務,這個方法會等待必定的時間,若是取不到任務就返回null。
而後判斷取到的任務r是否爲null,爲null則經過調用workerCanExit()方法來判斷當前worker是否能夠退出,咱們看一下workerCanExit()的實現:
private boolean workerCanExit() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
boolean canExit;
//若是runState大於等於STOP,或者任務緩存隊列爲空了
//或者 容許爲核心池線程設置空閒存活時間而且線程池中的線程數目大於1
try {
canExit = runState >= STOP ||
workQueue.isEmpty() ||
(allowCoreThreadTimeOut &&
poolSize > Math.max(1, corePoolSize));
} finally {
mainLock.unlock();
}
return canExit;
}
也就是說若是線程池處於STOP狀態、或者任務隊列已爲空或者容許爲核心池線程設置空閒存活時間而且線程數大於1時,容許worker退出。若是容許worker退出,則調用interruptIdleWorkers()中斷處於空閒狀態的worker,咱們看一下interruptIdleWorkers()的實現:
void interruptIdleWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) //實際上調用的是worker的interruptIfIdle()方法
w.interruptIfIdle();
} finally {
mainLock.unlock();
}
}
從實現能夠看出,它實際上調用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) { //注意這裏,是調用tryLock()來獲取鎖的,由於若是當前worker正在執行任務,鎖已經被獲取了,是沒法獲取到鎖的
//若是成功獲取了鎖,說明當前worker處於空閒狀態
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
這裏有一個很是巧妙的設計方式,假如咱們來設計線程池,可能會有一個任務分派線程,當發現有線程空閒時,就從任務緩存隊列中取一個任務交給空閒線程執行。可是在這裏,並無採用這樣的方式,
由於這樣會要額外地對任務分派線程進行管理,無形地會增長難度和複雜度,這裏直接讓執行完任務的線程去任務緩存隊列裏面取任務來執行。
咱們再看addIfUnderMaximumPoolSize方法的實現,這個方法的實現思想和addIfUnderCorePoolSize方法的實現思想很是類似,惟一的區別在於addIfUnderMaximumPoolSize方法是在線程池中的線程數達到了核心池大小而且往任務隊列中添加任務失敗的狀況下執行的:
private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < maximumPoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
看到沒有,其實它和addIfUnderCorePoolSize方法的實現基本如出一轍,只是if語句判斷條件中的poolSize < maximumPoolSize不一樣而已。
到這裏,大部分朋友應該對任務提交給線程池以後到被執行的整個過程有了一個基本的瞭解,下面總結一下:
1)首先,要清楚corePoolSize和maximumPoolSize的含義;
2)其次,要知道Worker是用來起到什麼做用的;
3)要知道任務提交給線程池以後的處理策略,這裏總結一下主要有4點:
若是當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會建立一個線程去執行這個任務;
若是當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(通常來講是任務緩存隊列已滿),則會嘗試建立新的線程去執行這個任務;
若是當前線程池中的線程數目達到maximumPoolSize,則會採起任務拒絕策略進行處理;
若是線程池中的線程數量大於corePoolSize時,若是某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;若是容許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。
3.線程池中的線程初始化
默認狀況下,建立線程池以後,線程池中是沒有線程的,須要提交任務以後纔會建立線程。
在實際中若是須要線程池建立以後當即建立線程,能夠經過如下兩個方法辦到:
prestartCoreThread():初始化一個核心線程;
prestartAllCoreThreads():初始化全部核心線程
下面是這2個方法的實現:
public boolean prestartCoreThread() {
return addIfUnderCorePoolSize(null); //注意傳進去的參數是null
}
public int prestartAllCoreThreads() {
int n = 0;
while (addIfUnderCorePoolSize(null))//注意傳進去的參數是null
++n;
return n;
}
注意上面傳進去的參數是null,根據第2小節的分析可知若是傳進去的參數爲null,則最後執行線程會阻塞在getTask方法中的
1
r = workQueue.take();
即等待任務隊列中有任務。
4.任務緩存隊列及排隊策略
在前面咱們屢次提到了任務緩存隊列,即workQueue,它用來存放等待執行的任務。
workQueue的類型爲BlockingQueue<Runnable>,一般能夠取下面三種類型:
1)ArrayBlockingQueue:基於數組的先進先出隊列,此隊列建立時必須指定大小;
2)LinkedBlockingQueue:基於鏈表的先進先出隊列,若是建立時沒有指定此隊列大小,則默認爲Integer.MAX_VALUE;
3)synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務。
5.任務拒絕策略
當線程池的任務緩存隊列已滿而且線程池中的線程數目達到maximumPoolSize,若是還有任務到來就會採起任務拒絕策略,一般有如下四種策略:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,可是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,而後從新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
6.線程池的關閉
ThreadPoolExecutor提供了兩個方法,用於線程池的關閉,分別是shutdown()和shutdownNow(),其中:
shutdown():不會當即終止線程池,而是要等全部任務緩存隊列中的任務都執行完後才終止,但不再會接受新的任務
shutdownNow():當即終止線程池,並嘗試打斷正在執行的任務,而且清空任務緩存隊列,返回還沒有執行的任務
7.線程池容量的動態調整
ThreadPoolExecutor提供了動態調整線程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:設置核心池大小
setMaximumPoolSize:設置線程池最大能建立的線程數目大小
當上述參數從小變大時,ThreadPoolExecutor進行線程賦值,還可能當即建立新的線程來執行任務。
三.使用示例
前面咱們討論了關於線程池的實現原理,這一節咱們來看一下它的具體使用:
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("線程池中線程數目:"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+
executor.getQueue().size()+",已執行玩別的任務數目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在執行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"執行完畢");
}
}
執行結果:
複製代碼
正在執行task 0
線程池中線程數目:1,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
線程池中線程數目:2,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 1
線程池中線程數目:3,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 2
線程池中線程數目:4,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 3
線程池中線程數目:5,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 4
線程池中線程數目:5,隊列中等待執行的任務數目:1,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:2,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:3,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:4,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
線程池中線程數目:6,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 10
線程池中線程數目:7,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 11
線程池中線程數目:8,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 12
線程池中線程數目:9,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 13
線程池中線程數目:10,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 14
task 3執行完畢
task 0執行完畢
task 2執行完畢
task 1執行完畢
正在執行task 8
正在執行task 7
正在執行task 6
正在執行task 5
task 4執行完畢
task 10執行完畢
task 11執行完畢
task 13執行完畢
task 12執行完畢
正在執行task 9
task 14執行完畢
task 8執行完畢
task 5執行完畢
task 7執行完畢
task 6執行完畢
task 9執行完畢
複製代碼
從執行結果能夠看出,當線程池中線程的數目大於5時,便將任務放入任務緩存隊列裏面,當任務緩存隊列滿了以後,便建立新的線程。若是上面程序中,將for循環中改爲執行20個任務,就會拋出任務拒絕異常了。
不過在java doc中,並不提倡咱們直接使用ThreadPoolExecutor,而是使用Executors類中提供的幾個靜態方法來建立線程池:
Executors.newCachedThreadPool(); //建立一個緩衝池,緩衝池容量大小爲Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //建立容量爲1的緩衝池
Executors.newFixedThreadPool(int); //建立固定容量大小的緩衝池
下面是這三個靜態方法的具體實現;
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
從它們的具體實現來看,它們實際上也是調用了ThreadPoolExecutor,只不過參數都已配置好了。
newFixedThreadPool建立的線程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
newSingleThreadExecutor將corePoolSize和maximumPoolSize都設置爲1,也使用的LinkedBlockingQueue;
newCachedThreadPool將corePoolSize設置爲0,將maximumPoolSize設置爲Integer.MAX_VALUE,使用的SynchronousQueue,也就是說來了任務就建立線程運行,當線程空閒超過60秒,就銷燬線程。
實際中,若是Executors提供的三個靜態方法能知足要求,就儘可能使用它提供的三個方法,由於本身去手動配置ThreadPoolExecutor的參數有點麻煩,要根據實際任務的類型和數量來進行配置。
另外,若是ThreadPoolExecutor達不到要求,能夠本身繼承ThreadPoolExecutor類進行重寫。
四.如何合理配置線程池的大小
本節來討論一個比較重要的話題:如何合理配置線程池大小,僅供參考。
通常須要根據任務的類型來配置線程池大小:
若是是CPU密集型任務,就須要儘可能壓榨CPU,參考值能夠設爲 NCPU+1
若是是IO密集型任務,參考值能夠設置爲2*NCPU
固然,這只是一個參考值,具體的設置還須要根據實際狀況進行調整,好比能夠先將線程池大小設置爲參考值,再觀察任務運行狀況和系統負載、資源利用率來進行適當調整。
參考資料:
http://ifeve.com/java-threadpool/
http://blog.163.com/among_1985/blog/static/275005232012618849266/
http://developer.51cto.com/art/201203/321885.htm
http://blog.csdn.net/java2000_wl/article/details/22097059
http://blog.csdn.net/cutesource/article/details/6061229
http://blog.csdn.net/xieyuooo/article/details/8718741
《JDK API 1.6》
4.java的堆與棧
4.1 java的內存分配策略
【靜態存儲區(方法區)】:主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,而且在程序整個運行期間都存在。
【棧區】:當方法被執行時,方法體內的局部變量(其中包括基礎數據類型、對象的引用)都在棧上建立,並在方法執行結束時這些局部變量所持有的內存將會自動被釋放。由於棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限。
【堆區】: 又稱動態內存分配,一般就是指在程序運行時直接 new 出來的對象和數組,也就是對象的實例。這部份內存在不使用時將會由 Java 垃圾回收器來負責回收。
4.2 棧和堆的區別
1、概述
在Java中,內存分爲兩種,一種是棧內存,另外一種就是堆內存。
2、堆內存
1.什麼是堆內存?
堆內存是是Java內存中的一種,它的做用是用於存儲Java中的對象和數組,當咱們new一個對象或者建立一個數組的時候,就會在堆內存中開闢一段空間給它,用於存放。
2.堆內存的特色是什麼?
第一點:堆其實能夠相似的看作是管道,或者說是平時去排隊買票的的狀況差很少,因此堆內存的特色就是:先進先出,後進後出,也就是你先排隊,好,你先買票。
第二點:堆能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,由於它是在運行時動態分配內存的,但缺點是,因爲要在運行時動態分配內存,存取速度較慢。
3.new對象在堆中如何分配?
由Java虛擬機的自動垃圾回收器來管理
3、棧內存
1.什麼是棧內存
棧內存是Java的另外一種內存,主要是用來執行程序用的,好比:基本類型的變量和對象的引用變量
2.棧內存的特色
第一點:棧內存就好像一個礦泉水瓶,像裏面放入東西,那麼先放入的沉入底部,因此它的特色是:先進後出,後進先出
第二點:存取速度比堆要快,僅次於寄存器,棧數據能夠共享,但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性
3.棧內存分配機制
棧內存能夠稱爲一級緩存,由垃圾回收器自動回收
4.數據共享
例子:
int a = 3;
int b = 3;
第一步處理:
1.編譯器先處理int a = 3;
2.建立變量a的引用
3.在棧中查找是否有3這個值
4.沒有找到,將3存放,a指向3
第二步處理:
1.處理b=3
2.建立變量b的引用
3.找到,直接賦值
第三步改變:
接下來
a = 4;
同上方法
a的值改變,a指向4,b的值是不會發生改變的
PS:若是是兩個對象的話,那就不同了,對象指向的是同一個引用,一個發生改變,另外一個也會發生改變
4、棧和堆的區別
JVM是基於堆棧的虛擬機.JVM爲每一個新建立的線程都分配一個堆棧.也就是說,對於一個Java程序來講,它的運行就是經過對堆棧的操做來完成的。堆棧以幀爲單位保存線程的狀態。JVM對堆棧只進行兩種操做:以幀爲單位的壓棧和出棧操做。
差別
1.堆內存用來存放由new建立的對象和數組
2.棧內存用來存放方法或者局部變量等
3.堆是先進先出,後進後出
4.棧是後進先出,先進後出
相同
1.都是屬於Java內存的一種
2.系統都會自動去回收它,可是對於堆內存通常開發人員會自動回收它
4.3 java的內存管理
Java的內存管理就是對象的分配和釋放問題。
在 Java 中,程序員須要經過關鍵字 new 爲每一個對象申請內存空間 (基本類型除外),全部的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執行的。
在 Java 中,內存的分配是由程序完成的,而內存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工做。
但同時,它也加劇了JVM的工做。這也是 Java 程序運行速度較慢的緣由之一。
由於,GC 爲了可以正確釋放對象,GC 必須監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都須要進行監控。
監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象再也不被引用。
爲了更好理解 GC 的工做原理,咱們能夠將對象考慮爲有向圖的頂點,將引用關係考慮爲圖的有向邊,有向邊從引用者指向被引對象。
另外,每一個線程對象能夠做爲一個圖的起始頂點,例如大多程序從 main 進程開始執行,那麼該圖就是以 main 進程頂點開始的一棵根樹。
在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。若是某個對象 (連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),
那麼咱們認爲這個(這些)對象再也不被引用,能夠被 GC 回收。如下,咱們舉一個例子說明如何用有向圖表示內存管理。
對於程序的每個時刻,咱們都有一個有向圖表示JVM的內存分配狀況。如下右圖,就是左邊程序運行到第6行的示意圖。
4.4 java的內存泄露
在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特色,
首先,這些對象是可達的,即在有向圖中,存在通路能夠與其相連;
其次,這些對象是無用的,即程序之後不會再使用這些對象。
若是對象知足這兩個條件,這些對象就能夠斷定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。
【java內存泄露的根本緣由】 長生命週期的對象持有短生命週期的對象的引用,短生命週期的引用已經無用了,可是仍然被持有而沒法釋放;
5.IO相關-Socket
5.1 網絡的基本認識
【IP地址】一個ip地址每每對應了一個服務器,或者說一臺主機。
【端口號】區分計算機上的軟件 至關於房門兩個字節0~65535 共65536個(2 的16次方)
邏輯端口是指邏輯意義上用於區分服務的端口,如TCP/IP協議中的服務端口,端口號的範圍從0到65535,好比用於瀏覽網頁服務的80端口,用於FTP服務的21端口等。
端口有什麼用呢?
咱們知道,一臺擁有IP地址的
主機能夠提供許多服務,好比Web服務、FTP服務、SMTP服務等,這些服務徹底能夠經過1個IP地址來實現。那麼,
主機是怎樣區分不一樣的網絡服務呢?
顯然不能只靠IP地址,由於IP 地址與網絡服務的關係是一對多的關係。其實是經過「IP地址+端口號」來區 分不一樣的服務的。
服務器通常都是經過知名端口號來識別的。例如,對於每一個TCP/IP實現來講,
FTP服務器的TCP端口號都是21,每一個Telnet服務器的TCP端口號都是23,每一個TFTP(簡單文件傳送協議)服務器的UDP端口號都是69。
任何TCP/IP實現所提供的服務都用知名的1~1023之間的端口號。這些知名端口號由Internet號分配機構(InternetAssignedNumbersAuthority,IANA)來管理。
到1992年爲止,知名端口號介於1~255之間。256~1023之間的端口號一般都是由Unix系統佔用,以提供一些特定的Unix服務—也就是說,提供一些只有Unix系統纔有的、而其餘
操做系統可能不提供的服務,IANA管理1~1023之間全部的端口號。
Internet擴展服務與Unix特定服務之間的一個差異就是Telnet和Rlogin。它們兩者都容許經過計算機網絡登陸到其餘
主機上。Telnet是採用端口號爲23的TCP/IP標準且幾乎能夠在全部
操做系統上進行實現。Rlogin只是爲Unix系統設計的(儘管許多非Unix系統也提供該服務),它的有名端口號爲513。
客戶端一般對它所使用的端口號並不關心,只需保證該端口號在本機上是惟一的就能夠了。
客戶端口號又稱做臨時端口號(即存在時間很短暫)。這是由於它一般只是在用戶運行該客戶程序時才存在,而服務器則只要
主機開着的,其服務就運行。
大多數TCP/IP實現給臨時端口分配1024~5000之間的端口號。大於5000的端口號是爲其餘服務器預留的(Internet上並不經常使用的服務)。咱們能夠在後面看見許多這樣的給臨時端口分配端口號的例子。
Solaris2.2是一個頗有名的例外。一般TCP和UDP的缺省臨時端口號從32768開始。
邏輯端口是邏輯上用於區分服務的端口。TCP/IP協議中的端口就是邏輯端口,經過不一樣的邏輯端口來區分不一樣的服務。一個IP地址的端口經過16bit進行編號,最多能夠有65536個端口。端口是經過端口號來標記的,端口號只有整數,範圍是從0 到65535。
TCP與UDP段結構中端口地址都是16比特,能夠有在0---65535範圍內的端口號。對於這65536個端口號有如下的使用規定:
(1)端口號小於256的定義爲經常使用端口,服務器通常都是經過經常使用端口號來識別的。任何TCP/IP實現所提供的服務都用1---1023之間的端口號,是由ICANN來管理的;
(2)客戶端只需保證該端口號在本機上是唯一的就能夠了。客戶端口號因存在時間很短暫又稱臨時端口號;
(3)大多數TCP/IP實現給臨時端口號分配1024---5000之間的端口號。大於5000的端口號是爲其餘服務器預留的。
【域名】域名是方便記憶的網址,
在訪問這些網址的時候會經過兩種方法轉換成IP地址才能鏈接到相應主機訪問相應的文件
【方法1】是經過,本機所特有的hosts文件,這個文件中定義了一些網址域名所對應的IP地址,當hosts文件中不存在這樣的對應關係時,就會採起
【方法2】另外一種萬能的方法,就是向不屬於本機的DNS服務器發送請求,DNS服務器進行解析變成IP地址返回,再訪問IP地址,從而達到目的。
【主機】主機一臺電腦,或者說是一臺有本身獨立IP地址的電腦,注意這裏電腦能夠是筆記本,臺式,甚至巨型計算機。
【tcp/udp協議】
UDP通信協議的特色:
1. 將數據極封裝爲數據包,面向無鏈接。
2. 每一個數據包大小限制在64K中
3.由於無鏈接,因此不可靠
4. 由於不須要創建鏈接,因此速度快
5.udp 通信是不分服務端與客戶端的,只分發送端與接收端。
還有TCP的特色以下:
TCP通信協議特色:
1. tcp是基於IO流進行數據 的傳輸的,面向鏈接。
2. tcp進行數據傳輸的時候是沒有大小限制的。
3. tcp是面向鏈接,經過三次握手的機制保證數據的完整性。 可靠協議。
4. tcp是面向鏈接的,因此速度慢。
5. tcp是區分客戶端與服務端 的。
順便附上咱們在利用JAVA寫兩個協議的過程:
UDP:
發送端的使用步驟:
1. 創建udp的服務。
2. 準備數據,把數據封裝到數據包中發送。 發送端的數據包要帶上ip地址與端口號。
3. 調用udp的服務,發送數據。
4. 關閉資源。
接收端的使用步驟:
1. 創建udp的服務
2. 準備空 的數據 包接收數據。
3. 調用udp的服務接收數據。
4. 關閉資源
TCP:
tcp的客戶端使用步驟:
1. 創建tcp的客戶端服務。
2. 獲取到對應的流對象。
3.寫出或讀取數據
4. 關閉資源。
ServerSocket的使用 步驟:
1. 創建tcp服務端 的服務。
2. 接受客戶端的鏈接產生一個Socket.
3. 獲取對應的流對象讀取或者寫出數據。
4. 關閉資源。
【url】統一資源定位符是對能夠從互聯網上獲得的資源的位置和訪問方法的一種簡潔的表示,是互聯網上標準資源的地址。
互聯網上的每一個文件都有一個惟一的URL,它包含的信息指出文件的位置以及瀏覽器應該怎麼處理它。
【InternetAddress】
InetAddress(IP類)
經常使用方法:
getLocalHost();//獲取本機的IP地址對象
getHostAddress(); //返回一個IP地址的字符串表示形式
getHostName();//獲取主機名
getByName("IP或者主機名");//根據IP地址的字符串形式或者主機名生成一個IP地址對象----很經常使用
UDP:
//發送端代碼
public class Sender{
public static void main(String[] args)
{
//創建udp服務
DatagramSocket datagramSocket = new DatagramSocket();
String data = @"upd的數據";
//把數據封裝到數據包中 DatagramPacket(buf, length, address) buf:發送的數據內容 length:發送數據內容的大小 address:發送目的IP地址對象 port:端口號
DatagramPacket packet = new DatagramPacket(data.getBytes(),data.getBytes().length,InetAddress.getLocalHost(),9091);
//發送數據包
datagramSocket.send(packet);
//關閉資源
datagramSocket.close();
}
}
----------
//接收端代碼
public class Receive {
public static void main(String[] args)
{
//創建udp服務,而且監聽一個端口
DatagramSocket socket = new DatagramSocket(9091);
//準備空的數據包用於存放數據
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf,buf.length);
//接收數據 數據實際上存儲在buf數組中 receive是一個阻塞型方法,沒有接收到數據就一直阻塞
socket.receive(packet);
//取出數據
String data = new String(buf,0,packet.getLength());//getLength() 獲取數據包存儲了幾個字節
//關閉資源
socket.close();
}
}
TCP:
1 //客戶端代碼
2 public class Client{
3
4 public static void main(String[] args)
5 {
6 //創建鏈接
7 Socket socket = new Socket(InetAddress.getLocalHost(),9090);
8 //獲取輸出流
9 OutputStream os = socket.getOutPutStream();
10 //向服務端發送數據
11 os.write("這是客戶端的數據".getBytes());
12 //關閉資源
13 socket.close();//由於os是從socket中獲取的,因此socket關閉的時候os也關閉了
14 }
15 }
16
17
18 ----------
19
20
21 //服務端代碼
22 public class Server{
23
24 public static void main(String[] args)
25 {
26 //創建tcp的服務端,而且監聽一個端口
27 ServerSocket serverSocket = new ServerSocket(9090);
28 //接受客戶端的鏈接
29 Socket socket = serverSocket.accept();//accept() 這是一個阻塞型的方法,沒有客戶端與其鏈接時,會一直等待下去
30 //獲取輸入流對象,讀取客戶端發送的內容
31 InputStream is = socket.getInputStream();
32 byte[] buf = new byte[1024];
33 int length = is.read(buf);
34 System.out.println("接收到的數據:"+new String(buf,0,length));
35 serverSocket.close();//由於socket是從serverSocket中獲取的,因此serverSocket關閉的時候socket也關閉了
36 }
37 }
5.2 建立socket的實例
主機 A 的應用程序要能和主機 B 的應用程序通訊,必須經過 Socket 創建鏈接,而創建 Socket 鏈接必須須要底層 TCP/IP 協議來創建 TCP 鏈接。
創建 TCP 鏈接須要底層 IP 協議來尋址網絡中的主機。
咱們知道網絡層使用的 IP 協議能夠幫助咱們根據 IP 地址來找到目標主機,可是一臺主機上可能運行着多個應用程序,
如何才能與指定的應用程序通訊就要經過 TCP 或 UPD 的地址也就是端口號來指定。
這樣就能夠經過一個 Socket 實例惟一表明一個主機上的一個應用程序的通訊鏈路了。
創建通訊鏈路
當客戶端要與服務端通訊,客戶端首先要建立一個 Socket 實例,操做系統將爲這個 Socket 實例分配一個沒有被使用的本地端口號,並建立一個包含本地和遠程地址和端口號的套接字數據結構,這個數據結構將一直保存在系統中直到這個鏈接關閉。在建立 Socket 實例的構造函數正確返回以前,將要進行 TCP 的三次握手協議,TCP 握手協議完成後,Socket 實例對象將建立完成,不然將拋出 IOException 錯誤。、
與之對應的服務端將建立一個 ServerSocket 實例,ServerSocket 建立比較簡單隻要指定的端口號沒有被佔用,通常實例建立都會成功,同時操做系統也會爲 ServerSocket 實例建立一個底層數據結構,這個數據結構中包含指定監聽的端口號和包含監聽地址的通配符,一般狀況下都是「*」即監聽全部地址。以後當調用 accept() 方法時,將進入阻塞狀態,等待客戶端的請求。當一個新的請求到來時,將爲這個鏈接建立一個新的套接字數據結構,該套接字數據的信息包含的地址和端口信息正是請求源地址和端口。這個新建立的數據結構將會關聯到 ServerSocket 實例的一個未完成的鏈接數據結構列表中,注意這時服務端與之對應的 Socket 實例並無完成建立,而要等到與客戶端的三次握手完成後,這個服務端的 Socket 實例纔會返回,並將這個 Socket 實例對應的數據結構從未完成列表中移到已完成列表中。因此 ServerSocket 所關聯的列表中每一個數據結構,都表明與一個客戶端的創建的 TCP 鏈接。
數據傳輸
傳輸數據是咱們創建鏈接的主要目的,如何經過 Socket 傳輸數據,下面將詳細介紹。
當鏈接已經創建成功,服務端和客戶端都會擁有一個 Socket 實例,每一個 Socket 實例都有一個 InputStream 和 OutputStream,正是經過這兩個對象來交換數據。同時咱們也知道網絡 I/O 都是以字節流傳輸的。當 Socket 對象建立時,操做系統將會爲 InputStream 和 OutputStream 分別分配必定大小的緩衝區,數據的寫入和讀取都是經過這個緩存區完成的。寫入端將數據寫到 OutputStream 對應的 SendQ 隊列中,當隊列填滿時,數據將被髮送到另外一端 InputStream 的 RecvQ 隊列中,若是這時 RecvQ 已經滿了,那麼 OutputStream 的 write 方法將會阻塞直到 RecvQ 隊列有足夠的空間容納 SendQ 發送的數據。值得特別注意的是,這個緩存區的大小以及寫入端的速度和讀取端的速度很是影響這個鏈接的數據傳輸效率,因爲可能會發生阻塞,因此網絡 I/O 與磁盤 I/O 在數據的寫入和讀取還要有一個協調的過程,若是兩邊同時傳送數據時可能會產生死鎖,在後面 NIO 部分將介紹避免這種狀況。
6.java的io接口
6.1 阻塞IO
BIO 帶來的挑戰
咱們知道阻塞I/O在調用InputStream.read()方法時是阻塞的,它會一直等到數據到來時(或超時)纔會返回;
一樣,在調用ServerSocket.accept()方法時,也會一直阻塞到有客戶端鏈接纔會返回,
每一個客戶端鏈接過來後,服務端都會啓動一個線程去處理該客戶端的請求。阻塞I/O的通訊模型示意圖以下:
若是你細細分析,必定會發現阻塞I/O存在一些缺點。根據阻塞I/O通訊模型,總結了它的兩點缺點:
1. 當客戶端多時,會
建立大量的處理線程。且
每一個線程都要佔用棧空間和一些CPU時間
2.
阻塞可能帶來頻繁的上下文切換,且大部分上下文切換多是無心義的。
【什麼是上下文?】
對於代碼中某個值來講,上下文是指這個值所在的局部(全局)做用域對象。
相對於進程而言,上下文就是進程執行時的環境,具體來講就是各個變量和數據,包括全部的寄存器變量、進程打開的文件、內存(堆棧)信息等。
阻塞I/O在調用InputStream.read()方法時是阻塞的,它會一直等到數據到來時(或超時)纔會返回;
一樣,在調用ServerSocket.accept()方法時,也會一直阻塞到有客戶端鏈接纔會返回,每一個客戶端鏈接過來後,服務端都會啓動一個線程去處理該客戶端的請求
BIO即阻塞I/O,無論是磁盤I/O仍是網絡I/O,數據在寫入OutputStream或者從InputStream讀取時都有可能會阻塞,一旦有阻塞,線程將會失去CPU的使用權,這在當前的大規模訪問量和有性能要求的狀況下是不能被接受的。雖然當前的網絡I/O有一些解決辦法,如一個客戶端一個處理線程,出現阻塞時只是一個線程阻塞而不會影響其餘線程工做,還有爲了減小系統線程的開銷,採用線程池的辦法來減小線程建立和回收的成本,可是有一些使用場景下仍然是沒法解決的。如當前一些須要大量HTTP長鏈接的狀況,像淘寶如今使用的Web旺旺,服務端須要同時保持幾百萬的HTTP鏈接,但並非每時每刻這些鏈接都在傳輸數據,這種狀況下不可能同時建立這麼多線程來保持鏈接。即便線程的數量不是問題,仍然有一些問題是沒法避免的,好比咱們想給某些客戶端更高的服務優先級,很難經過設計線程的優先級來完成。另一種狀況是,每一個客戶端的請求在服務端可能須要訪問一些競爭資源,這些客戶端在不一樣線程中,所以須要同步,要實現這種同步操做遠比用單線程複雜得多。以上這些狀況都說明,咱們須要另一種新的I/O操做方式
6.2 NIO
工做原理:
1. 由一個專門的線程來處理全部的 IO 事件,並負責分發。
2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。
3. 線程通信:線程之間經過 wait,notify 等方式通信。保證每次上下文切換都是有意義的。減小無謂的線程切換。
Java NIO的服務端只需啓動一個專門的線程來處理全部的 IO 事件,這種通訊模型是怎麼實現的呢?
java NIO採用了雙向通道(channel)進行數據傳輸,而不是單向的流(stream),在通道上能夠註冊咱們感興趣的事件。一共有如下四種事件:
事件名 |
對應值 |
服務端接收客戶端鏈接事件 |
SelectionKey.OP_ACCEPT(16) |
客戶端鏈接服務端事件 |
SelectionKey.OP_CONNECT(8) |
讀事件 |
SelectionKey.OP_READ(1) |
寫事件 |
SelectionKey.OP_WRITE(4) |
服務端和客戶端各自維護一個管理通道的對象,咱們稱之爲selector,該對象能檢測一個或多個通道 (channel) 上的事件。咱們以服務端爲例,若是服務端的selector上註冊了讀事件,某時刻客戶端給服務端發送了一些數據,阻塞I/O這時會調用read()方法 阻塞地讀取數據,
而NIO的服務端會在selector中添加一個讀事件。服務端的處理線程會輪詢地訪問selector,若是訪問selector時發 現有感興趣的事件到達,則處理這些事件,若是沒有感興趣的事件到達,則處理線程會一直阻塞直到感興趣的事件到達爲止。
1 package com.oztaking.www.myapplication;
2
3
4 import java.io.IOException;
5 import java.net.InetSocketAddress;
6 import java.nio.ByteBuffer;
7 import java.nio.channels.SelectionKey;
8 import java.nio.channels.Selector;
9 import java.nio.channels.ServerSocketChannel;
10 import java.nio.channels.SocketChannel;
11 import java.util.Iterator;
12
13 /**
14 * NIO服務端 17 */
18 public class NIOServer {
19 //通道管理器
20 private Selector selector;
21
22 /**
23 * 得到一個ServerSocket通道,並對該通道作一些初始化的工做
24 *
25 * @param port 綁定的端口號
26 * @throws IOException
27 */
28 public void initServer(int port) throws IOException {
29 // 得到一個ServerSocket通道
30 ServerSocketChannel serverChannel = ServerSocketChannel.open();
31 // 設置通道爲非阻塞
32 serverChannel.configureBlocking(false);
33 // 將該通道對應的ServerSocket綁定到port端口
34 serverChannel.socket().bind(new InetSocketAddress(port));
35 // 得到一個通道管理器
36 this.selector = Selector.open();
37 //將通道管理器和該通道綁定,併爲該通道註冊SelectionKey.OP_ACCEPT事件,註冊該事件後,
38 //當該事件到達時,selector.select()會返回,若是該事件沒到達selector.select()會一直阻塞。
39 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
40 }
41
42 /**
43 * 採用輪詢的方式監聽selector上是否有須要處理的事件,若是有,則進行處理
44 *
45 * @throws IOException
46 */
47 @SuppressWarnings("unchecked")
48 public void listen() throws IOException {
49 System.out.println("服務端啓動成功!");
50 // 輪詢訪問selector
51 while (true) {
52 //當註冊的事件到達時,方法返回;不然,該方法會一直阻塞
53 selector.select();
54 // 得到selector中選中的項的迭代器,選中的項爲註冊的事件
55 Iterator ite = this.selector.selectedKeys().iterator();
56 while (ite.hasNext()) {
57 SelectionKey key = (SelectionKey) ite.next();
58 // 刪除已選的key,以防重複處理
59 ite.remove();
60 // 客戶端請求鏈接事件
61 if (key.isAcceptable()) {
62 ServerSocketChannel server = (ServerSocketChannel) key
63 .channel();
64 // 得到和客戶端鏈接的通道
65 SocketChannel channel = server.accept();
66 // 設置成非阻塞
67 channel.configureBlocking(false);
68
69 //在這裏能夠給客戶端發送信息哦
70 channel.write(ByteBuffer.wrap(new String("向客戶端發送了一條信息").getBytes()));
71 //在和客戶端鏈接成功以後,爲了能夠接收到客戶端的信息,須要給通道設置讀的權限。
72 channel.register(this.selector, SelectionKey.OP_READ);
73
74 // 得到了可讀的事件
75 } else if (key.isReadable()) {
76 read(key);
77 }
78
79 }
81 }
82 }
83
84 /**
85 * 處理讀取客戶端發來的信息 的事件
86 *
87 * @param key
88 * @throws IOException
89 */
90 public void read(SelectionKey key) throws IOException {
91 // 服務器可讀取消息:獲得事件發生的Socket通道
92 SocketChannel channel = (SocketChannel) key.channel();
93 // 建立讀取的緩衝區
94 ByteBuffer buffer = ByteBuffer.allocate(10);
95 channel.read(buffer);
96 byte[] data = buffer.array();
97 String msg = new String(data).trim();
98 System.out.println("服務端收到信息:" + msg);
99 ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
100 channel.write(outBuffer);// 將消息回送給客戶端
101 }
102
103 /**
104 * 啓動服務端測試
105 *
106 * @throws IOException
107 */
108 public static void main(String[] args) throws IOException {
109 NIOServer server = new NIOServer();
110 server.initServer(8000);
111 server.listen();
112 }
113
114 }
115
118
119 客戶端:
120
121 package cn.nio;
122
123 import java.io.IOException;
124 import java.net.InetSocketAddress;
125 import java.nio.ByteBuffer;
126 import java.nio.channels.SelectionKey;
127 import java.nio.channels.Selector;
128 import java.nio.channels.SocketChannel;
129 import java.util.Iterator;
130
131 /**
132 * NIO客戶端
133 *135 */
136 public class NIOClient {
137 //通道管理器
138 private Selector selector;
139
140 /**
141 * 得到一個Socket通道,並對該通道作一些初始化的工做
142 *
143 * @param ip 鏈接的服務器的ip
144 * @param port 鏈接的服務器的端口號
145 * @throws IOException
146 */
147 public void initClient(String ip, int port) throws IOException {
148 // 得到一個Socket通道
149 SocketChannel channel = SocketChannel.open();
150 // 設置通道爲非阻塞
151 channel.configureBlocking(false);
152 // 得到一個通道管理器
153 this.selector = Selector.open();
154
155 // 客戶端鏈接服務器,其實方法執行並無實現鏈接,須要在listen()方法中調
156 //用channel.finishConnect();才能完成鏈接
157 channel.connect(new InetSocketAddress(ip, port));
158 //將通道管理器和該通道綁定,併爲該通道註冊SelectionKey.OP_CONNECT事件。
159 channel.register(selector, SelectionKey.OP_CONNECT);
160 }
161
162 /**
163 * 採用輪詢的方式監聽selector上是否有須要處理的事件,若是有,則進行處理
164 *
165 * @throws IOException
166 */
167 @SuppressWarnings("unchecked")
168 public void listen() throws IOException {
169 // 輪詢訪問selector
170 while (true) {
171 selector.select();
172 // 得到selector中選中的項的迭代器
173 Iterator ite = this.selector.selectedKeys().iterator();
174 while (ite.hasNext()) {
175 SelectionKey key = (SelectionKey) ite.next();
176 // 刪除已選的key,以防重複處理
177 ite.remove();
178 // 鏈接事件發生
179 if (key.isConnectable()) {
180 SocketChannel channel = (SocketChannel) key
181 .channel();
182 // 若是正在鏈接,則完成鏈接
183 if (channel.isConnectionPending()) {
184 channel.finishConnect();
185
186 }
187 // 設置成非阻塞
188 channel.configureBlocking(false);
189
190 //在這裏能夠給服務端發送信息哦
191 channel.write(ByteBuffer.wrap(new String("向服務端發送了一條信息").getBytes()));
192 //在和服務端鏈接成功以後,爲了能夠接收到服務端的信息,須要給通道設置讀的權限。
193 channel.register(this.selector, SelectionKey.OP_READ);
194
195 // 得到了可讀的事件
196 } else if (key.isReadable()) {
197 read(key);
198 }
199
200 }
201
202 }
203 }
204
205 /**
206 * 處理讀取服務端發來的信息 的事件
207 *
208 * @param key
209 * @throws IOException
210 */
211 public void read(SelectionKey key) throws IOException {
212 //和服務端的read方法同樣
213 }
214
215
216 /**
217 * 啓動客戶端測試
218 *
219 * @throws IOException
220 */
221 public static void main(String[] args) throws IOException {
222 NIOClient client = new NIOClient();
223 client.initClient("localhost", 8000);
224 client.listen();
225 }
226
227 }
7.異常相關
java 異常是程序運行過程當中出現的錯誤。Java把異常看成對象來處理,並定義一個基類java.lang.Throwable做爲全部異常的超類。
在Java API中定義了許多異常類,分爲兩大類,錯誤Error和異常Exception。其中異常類Exception又分爲運行時異常(RuntimeException)和非運行時異常(非runtimeException),
也稱之爲不檢查異常(Unchecked Exception)和檢查異常(Checked Exception)。
7.一、Error與Exception
Error是程序沒法處理的錯誤,好比OutOfMemoryError、ThreadDeath等。
這些異常發生時,Java虛擬機(JVM)通常會選擇線程終止。
Exception是程序自己能夠處理的異常,這種異常分兩大類運行時異常和非運行時異常。程序中應當儘量去處理這些異常。
7.二、運行時異常和非運行時異常
運行時異常: 都是RuntimeException類及其子類異常:
IndexOutOfBoundsException索引越界異常
ArithmeticException:數學計算異常
NullPointerException:空指針異常
ArrayOutOfBoundsException:數組索引越界異常
ClassNotFoundException:類文件未找到異常
ClassCastException 類型轉換異常
這些異常是不檢查異常(Unchecked Exception),程序中能夠選擇捕獲處理,也能夠不處理。這些異常通常是由程序邏輯錯誤引發的。
非運行時異常:是RuntimeException之外的異常,類型上都屬於Exception類及其子類。從程序語法角度講是必須進行處理的異常,若是不處理,程序就不能編譯經過。如:
IOException、文件讀寫異常
FileNotFoundException:文件未找到異常
EOFException:讀寫文件尾異常
MalformedURLException:URL格式錯誤異常
SocketException:Socket異常
SQLException:SQL數據庫異常
7.三、異常的捕獲和處理
一、try···catch語句
在try代碼塊中拋出異常以後,當即轉到catch代碼塊執行或者退棧到上一層方法處尋找catch代碼塊。
二、finally語句:任何狀況下都必須執行的代碼
因爲異常會強制中斷正常流程,這會使得某些無論在任何狀況下都必須執行的步驟被忽略,從而影響程序的健壯性。
使用finally語句,無論try代碼塊中是否出現了異常,都會執行finally代碼塊。
在某些狀況下,把finally的操做放在try···catch語句的後面,這也能保證這個操做被執行。
這種狀況儘管在某些狀況下是可行的,但不值得推薦,覺得它有兩個缺點:
@把與try代碼塊相關的操做孤立開來,使程序結構鬆散,可讀性差。
@影響程序的健壯性。假如catch代碼塊繼續拋出異常,就不會執行catch代碼塊以後的操做。
三、throws子句:聲明可能會出現的異常
若是一個方法可能會拋出異常,但沒有能力來處理這種異常,能夠在方法聲明處用throws子句來聲明拋出異常。
一個方法可能會出現多種異常,throws子句容許聲明拋出多個異常,中間用「,」隔開。
異常聲明是接口(概念上的接口)的一部分,在JavaDoc文檔中應描述方法可能拋出某種異常的條件。根據異常聲明,方法調用者瞭解到被調用方法可能拋出的異常,從而採起相應的措施:捕獲異常,或者聲明繼續拋出異常。
四、throw語句:拋出異常
throw語句用於拋出異常。
值得注意的是,有throw語句拋出的對象必須是java.lang.Throwable類或者其餘子類的實例。
7.4 異常調用處理的流程
Java虛擬機用方法調用棧(method invocation stack)來跟蹤每一個線程中一系列的方法調用過程。
該堆棧保存了每一個調用方法的本地信息(好比方法的局部變量)。
每一個線程都有一個獨立的方法調用棧。
對於Java應用程序的主線程,堆棧底部是程序的入口方法main()。
當一個新方法被調用時,Java虛擬機把描述該方法的棧結構置入棧頂,位於棧頂的方法爲正在執行的方法。
當一個方法正常執行完畢,Java虛擬機會從調用棧中彈出該方法的棧結構,而後繼續處理前一個方法。
若是在執行方法的過程當中拋出異常,則Java虛擬機必須找到能捕獲該異常的catch代碼塊。
它首先查看當前方法是否存在這樣的catch代碼塊,若是存在,那麼就執行該catch代碼塊;
不然,Java虛擬機會從調用棧中彈出該方法的棧結構,繼續到前一個方法中查找合適的catch代碼塊。
在回溯過程當中,若是Java虛擬機在某個方法中找到了處理該異常的代碼塊,則該方法的棧結構將成爲棧頂元素,程序流程將轉到該方法的異常處理代碼部分繼續執行。
當Java虛擬機追溯到調用棧的底部的方法時,若是仍然沒有找處處理該異常的代碼塊,按如下步驟處理。
(1)調用異常對象的printStackTrace()方法,打印來自方法調用棧的異常信息。
(2)若是該線程不是主線程,那麼終止這個線程,其餘線程繼續正常運行。若是該線程是主線程(即方法調用棧的底部爲main()方法),那麼整個應用程序被終止。
7.5 異常的運行流程
異常流程有try···catch···finally語句來控制。若是程序中還包含了return和System.exit()語句,就會使流程變得更加複雜。
(1)finally語句不被執行的惟一狀況就是先執行了用於終止程序的System.exit()方法。
java.lang.System類的靜態方法exit()用於終止當前的Java虛擬機進程,Java虛擬機所執行的Java程序也隨之終止。
另外,當catch語句中也拋出異常的狀況下,在程序退棧尋找上一個方法的catch代碼塊以前,會先執行finally代碼塊。
(2)return語句用於退出本方法。在執行try或catch代碼塊中的return語句時,假若有finally代碼塊,會先執行finally代碼塊。
(3)finally代碼塊雖然在return語句以前就被執行(這裏是指在return返回以前執行,若是return a+b;那麼是先執行了a+b,再執行finally代碼塊,再返回),
但finally代碼塊不能經過從新給變量賦值的方式改變return語句的返回值。
在這裏涉及到return語句的機制,若是return a;
a是基本類型的變量,這裏能夠理解的是傳值,a的值賦給了一個不知名的變量,return將這個不知名的變量內容返回,因此finally語句只能更改a的內容,不能更改那個和a的值相同的不知名變量的值,因此return的結果不能夠被finally中的代碼改變;
可是若是a是引用類型的變量,這裏就不是傳值了而是傳的引用,這樣不知名的變量和a都指向了同一個對象,咱們能夠經過引用a來改變這個對象,使得這個不知名變量所引用的對象發生改變,一樣也不能改變這個不知名變量的內容,
它仍然指向這個對象,咱們不可讓它指向其餘對象或者變成null,由於咱們不知道這個不知名變量的名字。
(4)建議不要在finally代碼塊中使用return語句,覺得它會致使如下兩種潛在的錯誤。
第一種錯誤是覆蓋try或catch代碼塊的return語句。能夠這樣理解,在try或者catch中的return在把返回的結果賦給一個不知名的臨時變量後,執行finally,若是沒有finally裏的return語句,接着回來將這個不知名變量的內容返回,若是在finally中出現了return語句,那麼這個return語句沒有被打斷,給另外一個不知名變量賦值以後,直接返回了,方法退棧,try或catch裏的返回沒有別執行,這樣的結果就是finally中的return覆蓋了try和catch中的return語句。
第二種錯誤是丟失異常。若是catch代碼塊中有throw語句拋出異常,因爲先執行了finally代碼塊,又由於finally代碼塊中有return語句,因此方法退棧,catch代碼塊中的throw語句就沒有被執行。
7.6 常見的異常的面試題
【1】java中的檢查型異常和非檢查型異常有什麼區別?(常考)
Java裏面異常分爲兩大類:checkedexception(檢查異常)和unchecked exception(未檢查異常),
對於未檢查異常也叫RuntimeException(運行時異常),對於運行時異常,java編譯器不要求必定要把它捕獲或者必定要繼續拋出,
可是對checkedexception(檢查異常)要求必需要在方法裏面或者捕獲或者繼續拋出。
非檢測異常拋出時,可不聲明不使用try-catch語句;對於檢測異常拋出時須聲明且使用try-catch語句
Java的可檢測異常和非檢測異常涇渭分明。
可檢測異常經編譯器驗證,對於聲明拋出異常的任何方法,編譯器將強制執行處理或聲明規則。
非檢測異常不遵循處理或聲明規則。
在產生此類異常時,不必定非要採起任何適當操做,編譯器不會檢查是否已解決了這樣一個異常。有兩個主要類定義非檢測異常:RuntimeException和Error。
爲何Error子類屬於非檢測異常?這是由於沒法預知它們的產生時間。若Java應用程序內存不足,則隨時可能出現OutOfMemoryError;原由通常不是應用程序中的特殊調用,而是JVM自身的問題。另外,Error類通常表示應用程序沒法解決的嚴重問題,故將這些類視爲非檢測異常。
RuntimeException類也屬於非檢測異常,一個緣由是普通JVM操做引起的運行時異常隨時可能發生。與Error不一樣,此類異常通常由特定操做引起。但這些操做在Java應用程序中會頻繁出現。例如,若每次使用對象時,都必須編寫異常處理代碼來檢查null引用,
則整個應用程序很快將變成一個龐大的try-catch塊。所以,運行時異常不受編譯器檢查與處理或聲明規則的限制。
將RuntimeException類做爲未檢測異常還有一個緣由:它們表示的問題不必定做爲異常處理。能夠在try-catch結構中處理NullPointerException,但若在使用引用前測試空值,則更簡單,更經濟。一樣,能夠在除法運算時檢查0值,而不使用ArithmeticException。
【2】throw和throws的區別
一、throws出如今方法頭;而throw出如今方法體。
二、throws表示出現異常的一種可能性,並不必定會發生這些異常;throw則是拋出了異常,執行throw則必定拋出了某種異常對象。
三、二者都是消極處理異常的方式(這裏的消極並非說這種方式很差),只是拋出或者可能拋出異常,可是不會由方法去處理異常,真正的處理異常由方法的上層調用處理。
好的編程習慣:
1.在寫程序時,對可能會出現異常的部分一般要用try{...}catch{...}去捕捉它並對它進行處理;
2.用try{...}catch{...}捕捉了異常以後必定要對在catch{...}中對其進行處理,那怕是最簡單的一句輸出語句,或棧輸入e.printStackTrace();
3.若是是捕捉IO輸入輸出流中的異常,必定要在try{...}catch{...}後加finally{...}把輸入輸出流關閉
4.若是方法體內用throw拋出了某種異常,最好要在方法名中加throws拋異常聲明,而後交給調用它的上層函數進行處理。
【3】若是執行finally代碼塊以前方法返回告終果/JVM退出,finally代碼塊會執行嗎?
【答】在執行finally代碼塊以前方法返回結果是會執行finally代碼塊的;
可是JVM退出則不會執行finally代碼塊;
finally代碼塊不被執行的三種狀況:
【狀況1】在try..代碼塊以外異常或者返回,不會執行finally代碼塊;
【狀況2】在執行try代碼塊的時候退出了jvm虛擬機則不會執行finally代碼塊
【狀況3】在子線程中執行try代碼塊忽然關閉了子線程;
【4】java中final finally finalize 關鍵字的區別?
一、final
Final能夠用於成員變量(包括方法參數),方法、類。
Final成員
變量一旦被初始化便不可改變(對於基本類型,指的是值不變;對於對象類型,指的是引用不變),初始化只可能在兩個地方:定義處和構造函數。
對於基本類型,定義成final參數沒有什麼意義,由於基本類型就是傳值,不會影響調用語句中的變量;
對於對象類型,在方法中若是參數確認不須要改變時,定義成final參數能夠防止方法中無心的修改而影響到調用方法。
Final方法
- 不可覆寫
- 編譯器將對此方法的調用轉化成行內(inline)調用,即直接把方法主體插入到調用處(方法主體內容過多的時候反而會影響效率)
Final類
二、finally
異常處理關鍵字,finally中的主體總會執行,無論異常發生是否。
2.一、當try中有return時執行順序
return語句並非函數的最終出口,若是有finally語句,這在return以後還會執行finally(return的值會暫存在棧裏面,等待finally執行後再返回)
2.二、return和異常獲取語句的位置
- 狀況一(try中有return,finally中沒有return)
「return num += 80」被拆分紅了「num = num+80」和「return num」兩個語句,線執行try中的「num =num+80」語句,將其保存起來,在try中的」return num「執行前,先將finally中的語句執行完,然後再將90返回。
- 狀況二(try和finally中均有return)
try中的return被」覆蓋「掉了,再也不執行。
雖然在finally中改變了返回值num,但由於finally中沒有return該num的值,所以在執行完finally中的語句後,test()函數會獲得try中返回的num的值,而try中的num的值依然是程序進入finally代碼塊前保留下來的值,所以獲得的返回值爲10。而且函數最後面的return語句不會執行。
6.2.三、簡單地總結以下:
try語句在返回前,將其餘全部的操做執行完,保留好要返回的值,然後轉入執行finally中的語句,然後分爲如下三種狀況:
狀況一:若是finally中有return語句,則會將try中的return語句」覆蓋「掉,直接執行finally中的return語句,獲得返回值,這樣便沒法獲得try以前保留好的返回值。
狀況二:若是finally中沒有return語句,也沒有改變要返回值,則執行完finally中的語句後,會接着執行try中的return語句,返回以前保留的值。
狀況三:若是finally中沒有return語句,可是改變了要返回的值,這裏有點相似與引用傳遞和值傳遞的區別,分如下兩種狀況,:
- 若是return的數據是基本數據類型,則在finally中對該基本數據的改變不起做用,try中的return語句依然會返回進入finally塊以前保留的值。
- 若是return的數據是引用數據類型,而在finally中對該引用數據類型的屬性值的改變起做用,try中的return語句返回的就是在finally中改變後的該屬性的值。
6.三、finalize
類的Finalize方法,能夠告訴垃圾回收器應該執行的操做,該方法從Object類繼承而來。
在從堆中永久刪除對象以前,垃圾回收器調用該對象的Finalize方法。
finalize()是Object的protected方法,子類能夠覆蓋該方法以實現資源清理工做,GC在回收對象以前調用該方法
注意,沒法確切地保證垃圾回收器什麼時候調用該方法,也沒法保證調用不一樣對象的方法的順序。
即便一個對象包含另外一個對象的引用,或者在釋放一個對象好久之前就釋放了另外一個對象,也可能會以任意的順序調用這兩個對象的Finalize方法。
若是必須保證採用特定的順序,則必須提供本身的特有清理方法。
7.註解相關
7.1.什麼是註解
常見的做用有如下幾種:
1.生成文檔。這是最多見的,也是java 最先提供的註解。經常使用的有@see @param @return 等;
2.跟蹤代碼依賴性,實現替代配置文件功能。比較常見的是spring 2.5 開始的基於註解配置。做用就是減小配置。如今的框架基本都使用了這種配置來減小配置文件的數量;
3.在編譯時進行格式檢查。如@Override放在方法前,若是你這個方法並非覆蓋了超類方法,則編譯時就能檢查出;
包 java.lang.annotation 中包含全部定義自定義註解所需用到的原註解和接口。如接口 java.lang.annotation.Annotation 是全部註解繼承的接口,而且是自動繼承,不須要定義時指定,相似於全部類都自動繼承Object。
該包同時定義了四個元註解,
Documented,
Inherited,
Target(做用範圍,方法,屬性,構造方法等),
Retention(生命範圍,源代碼,class,runtime)。
下面將在實例中逐個講解他們的做用及使用方法。
Inherited : 在您定義註解後並使用於程序代碼上時,預設上父類別中的註解並不會被繼承至子類別中,您能夠在定義註解時加上java.lang.annotation.
Inherited 限定的Annotation,這讓您定義的Annotation型別被繼承下來。注意註解繼承只針對class 級別註解有效(這段建議看徹底文後在來回顧)。
多說無益,下面就一步步從零開始建一個咱們本身的註解。
自定義一個註解
package com.tmser.annotation;
public @interface TestA {
//這裏定義了一個空的註解,它能幹什麼呢。我也不知道,但他能用。
}
在下面這個程序中使用它:
package com.tmser.annotation;
import java.util.HashMap;
import java.util.Map;
@TestA //使用了類註解
public class UserAnnotation {
@TestA //使用了類成員註解
private Integer age;
@TestA //使用了構造方法註解
public UserAnnotation(){
}
@TestA //使用了類方法註解
public void a(){
@TestA //使用了局部變量註解
Map m = new HashMap(0);
}
public void b(@TestA Integer a){ //使用了方法參數註解
}
}
編譯沒有報錯,ok,一個註解實驗完成。這個註解也太簡單了吧,好像什麼信息也不能傳遞。別急下面就來一步步完善它,也該四位元註解依次開始上場了。
四個元註解分別是:@Target,@Retention,@Documented,@Inherited ,再次強調下元註解是java API提供,是專門用來定義註解的註解,其做用分別以下:
@Target 表示該註解用於什麼地方,可能的值在枚舉類 ElemenetType 中,包括:
ElemenetType.CONSTRUCTOR----------------------------構造器聲明
ElemenetType.FIELD --------------------------------------域聲明(包括 enum 實例)
ElemenetType.LOCAL_VARIABLE------------------------- 局部變量聲明
ElemenetType.METHOD ----------------------------------方法聲明
ElemenetType.PACKAGE --------------------------------- 包聲明
ElemenetType.PARAMETER ------------------------------參數聲明
ElemenetType.TYPE--------------------------------------- 類,接口(包括註解類型)或enum聲明
@Retention 表示在什麼級別保存該註解信息。可選的參數值在枚舉類型 RetentionPolicy 中,包括:
RetentionPolicy.SOURCE ---------------------------------註解將被編譯器丟棄
RetentionPolicy.CLASS -----------------------------------註解在class文件中可用,但會被VM丟棄
RetentionPolicy.RUNTIME VM-------將在運行期也保留註釋,所以能夠經過反射機制讀取註解的信息。
@Documented 將此註解包含在 javadoc 中 ,它表明着此註解會被javadoc工具提取成文檔。在doc文檔中的內容會由於此註解的信息內容不一樣而不一樣。至關與@see,@param 等。
@Inherited 容許子類繼承父類中的註解。
學習最忌好高騖遠,最重要的仍是動手實踐,咱們就一個一個來實驗。
第一個:@Target,動手在前面咱們編寫的註解上加上元註解。
package com.tmser.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.PACKAGE) //與前面定義的註解不一樣的地方,這裏使用了元註解Target
public @interface TestA {
}
ctrl+ s 保存,今天電腦比較給力,咱們的測試類那邊立馬出現了一堆錯誤,除了類註解。我想到這,聰明的你馬上明白了這個元註解的意義了。是否是想固然的偷起懶來了。
?難道還有意外?細心的朋友應該發現了,咱們的測試類少了一個屬性沒用,就是ElemenetType.PACKAGE。在咱們的註解加上這個屬性的元註解後,咱們測試程序的元註解所有陣亡,不對,還有一個沒加呢,好加上。package 包,想固然是加載 package 前面。即
@TestA package com.tmser.annotation;
什麼也報錯。這就搞不明白了,不加在這加哪去呢。我也不知道了,不過這是編譯錯誤,咱們的eclipse 將錯誤給咱們指出了,就是Package annotations must be in file package-info.java ,e 文雖然很差,
但這個簡單的仍是難不倒幾我的的,package 註解必須定義在 package-info.java 中。package-info 又是什麼東西,好了爲節省大家的時間幫你百度好了(在另外一篇個人另外一篇博文裏面,本身找吧)。ok,到此 target 元註解就所有完成了。
第二個元註解: @Retention 參數 RetentionPolicy。有了前面的經驗這個註解理解起來就簡單多了,而且幸運的是這個註解尚未特殊的屬性值。 簡單演示下如何使用:
package com.tmser.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestA {
}
第三和第四個元註解就再也不舉例了。比較簡單,也沒有值,相信看過上面的解釋也就清楚了。下面咱們仍是繼續來深刻的探討下註解的使用。上面的例子都很是簡單,註解連屬性都沒有。ok,下面咱們就來定義一個有屬性的註解,並在例子程序中獲取都註解中定義的值。
開始以前將下定義屬性的規則:
@interface用來聲明一個註解,其中的每個方法其實是聲明瞭一個配置參數。方法的名稱就是參數的名稱,返回值類型就是參數的類型(返回值類型只能是基本類型、Class、String、enum)。能夠經過default來聲明參數的默認值。
代碼:
@Target({TYPE,METHOD,FIELD,CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestA {
String name();
int id() default 0;
Class gid();
}
下面改下咱們的測試類:
package com.tmser.annotation;
import java.util.HashMap;
import java.util.Map;
@TestA(name="type",gid=Long.class) //類成員註解
public class UserAnnotation {
@TestA(name="param",id=1,gid=Long.class) //類成員註解
private Integer age;
@TestA (name="construct",id=2,gid=Long.class)//構造方法註解
public UserAnnotation(){
}
@TestA(name="public method",id=3,gid=Long.class) //類方法註解
public void a(){
Map m = new HashMap(0);
}
@TestA(name="protected method",id=4,gid=Long.class) //類方法註解
protected void b(){
Map m = new HashMap(0);
}
@TestA(name="private method",id=5,gid=Long.class) //類方法註解
private void c(){
Map m = new HashMap(0);
}
public void b(Integer a){
}
}
下面到了最重要的一步了,就是如何讀取咱們在類中定義的註解。只要讀取出來了使用的話就簡單了。
package com.tmser.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ParseAnnotation {
public static void parseTypeAnnotation() throws ClassNotFoundException {
Class clazz = Class.forName("com.tmser.annotation.UserAnnotation");
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
TestA testA = (TestA)annotation;
System.out.println("id= ""+testA.id()+""; name= ""+testA.name()+""; gid = "+testA.gid());
}
}
public static void parseMethodAnnotation(){
Method[] methods = UserAnnotation.class.getDeclaredMethods();
for (Method method : methods) {
boolean hasAnnotation = method.isAnnotationPresent(TestA.class);
if (hasAnnotation) {
TestA annotation = method.getAnnotation(TestA.class);
System.out.println("method = " + method.getName()
+ " ; id = " + annotation.id() + " ; description = "
+ annotation.name() + "; gid= "+annotation.gid());
}
}
}
public static void parseConstructAnnotation(){
Constructor[] constructors = UserAnnotation.class.getConstructors();
for (Constructor constructor : constructors) {
boolean hasAnnotation = constructor.isAnnotationPresent(TestA.class);
if (hasAnnotation) {
TestA annotation =(TestA) constructor.getAnnotation(TestA.class);
System.out.println("constructor = " + constructor.getName()
+ " ; id = " + annotation.id() + " ; description = "
+ annotation.name() + "; gid= "+annotation.gid());
}
}
}
public static void main(String[] args) throws ClassNotFoundException {
parseTypeAnnotation();
parseMethodAnnotation();
parseConstructAnnotation();
}
}
先別說話,運行:
id= 「0」; name= 「type」; gid = class java.lang.Long method = c ; id = 5 ; description = private method; gid= class java.lang.Long method = a ; id = 3 ; description = public method; gid= class java.lang.Long method
= b ; id = 4 ; description = protected method; gid= class java.lang.Long constructor = com.tmser.annotation.UserAnnotation ; id
= 2 ; description = construct; gid= class java.lang.Long
看到了吧,咱們定義的註解都完整的輸出了,你要使用哪一個,直接拿去用就行了。
爲了避免讓這篇文章打開太慢,我省略了類屬性註解,及參數註解的解析。其實都大同小異。
另外,我也沒有舉使用例子。由於我認爲好的教程是講的詳細的同時,還會留有擴展。若是我所有寫出來,而你只是學習的話,那基本不會本身去動腦了,而是複製粘貼運行一遍完事。
最後提醒下:
要用好註解,必須熟悉java 的反射機制,從上面的例子能夠看出,註解的解析徹底依賴於反射。
不要濫用註解。日常咱們編程過程不多接觸和使用註解,只有作設計,且不想讓設計有過多的配置時
這個網址能夠給你參考一些註解的例子:http://blog.sina.com.cn/s/blog_7540bf5f0100t3mv.html
轉自:http://blog.sina.com.cn/s/blog_93dc666c0101gzn5.html
元數據是指用來描述數據的數據,更通俗一點,就是描述代碼間關係,或者代碼與其餘資源(例如數據庫表)之間內在聯繫的數據。元數據的功能做用有不少,
好比:你可能用過Javadoc的註釋自動生成文檔。這就是元數據功能的一種。
總的來講,元數據能夠用來建立文檔,跟蹤代碼的依賴性,執行編譯時格式檢查,代替已有的配置文件。
在Java中元數據以標籤的形式存在於Java代碼中,元數據標籤的存在並不影響程序代碼的編譯和執行,它只是被用來生成其它的文件或針在運行時知道被運行代碼的描述信息。
綜上所述:
第一,元數據以標籤的形式存在於Java代碼中。
第二,元數據描述的信息是類型安全的,即元數據內部的字段都是有明確類型的。
第三,元數據須要編譯器以外的工具額外的處理用來生成其它的程序部件。
第四,元數據能夠只存在於Java源代碼級別,也能夠存在於編譯以後的Class文件內部。
7.3.註解分類
1)系統內置標準註解:
a.Override,限定重寫父類方法
@Override 是一個標記註解類型,它被用做標註方法。它說明了被標註的方法重載了父類的方法,起到了斷言的做用。
若是咱們使用了這種Annotation在一個沒有覆蓋父類方法的方法時,java編譯器將以一個編譯錯誤來警示。
這個annotaton經常在咱們試圖覆蓋父類方法而確又寫錯了方法名時發揮威力。
使用方法極其簡單:在使用此annotation時只要在被修飾的方法前面加上@Override便可。
這裏咱們定義了一個父類father 裏面定義了一個方法getName,定義了一個son類,裏面複寫了父類的getName方法並用Override,限定重寫父類方法
b.@Deprecated,標記已過期:
同 樣Deprecated也是一個標記註解。
當一個類型或者類型成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標註的程序元素。
並且這種修飾具備必定的 「延續性」:若是咱們在代碼中經過繼承或者覆蓋的方式使用了這個過期的類型或者成員,雖然繼承或者覆蓋後的類型或者成員並非被聲明爲 @Deprecated,但編譯器仍然要報警。
這裏咱們看下代碼:定義了一個ParentDeprecated,並把他標記爲過期Deprecated,而後咱們就能發現ide工具在他的類名上劃了一條橫線,並且當我定義他的子類childDeprecated時,發如今她的類名前也會被代表過期。
最後,值得注意,@Deprecated這個annotation類型和javadoc中的 @deprecated這個tag是有區別的:前者是java編譯器識別的,然後者是被javadoc工具所識別用來生成文檔(包含程序成員爲何已通過 時、它應當如何被禁止或者替代的描述)。
c.SuppressWarnnings,抑制編譯器警告:
@SuppressWarnings 被用於有選擇的關閉編譯器對類、方法、成員變量、變量初始化的警告。在java5.0,sun提供的javac編譯器爲咱們提供了-Xlint選項來使編譯器對合法的程序代碼提出警告,此種警告從某種程度上表明瞭程序錯誤。例如當咱們使用一個generic collection類而又沒有提供它的類型時,編譯器將提示出"unchecked warning"的警告。一般當這種狀況發生時,咱們就須要查找引發警告的代碼。若是它真的表示錯誤,咱們就須要糾正它。例如若是警告信息代表咱們代碼中的switch語句沒有覆蓋全部可能的case,那麼咱們就應增長一個默認的case來避免這種警告。
有時咱們沒法避免這種警告,例如,咱們使用必須和非generic的舊代碼交互的generic collection類時,咱們不能避免這個unchecked warning。此時@SuppressWarning就要派上用場了,在調用的方法前增長@SuppressWarnings修飾,告訴編譯器中止對此方法的警告。
SuppressWarnings註解的常見參數值的簡單說明:
1.deprecation:使用了不同意使用的類或方法時的警告;
2.unchecked:執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合保存的類型;
當咱們不使用@SuppressWarnings註釋時,編譯器就會有以下提示:
注意:SuppressWarningsDemo.java 使用了未經檢查或不安全的操做。
注意:要了解詳細信息,請使用 -Xlint:unchecked 從新編譯。
SuppressWarnings 被用於有選擇的關閉編譯器對類、方法、成員變量、變量初始化的警告
Overload是重載的意思,Override是覆蓋的意思,也就是重寫。
重載Overload:在同一個類中,容許存在一個以上的同名函數,只要他們的參數個數或者參數類型不一樣便可。
重載的特色:與返回值類型無關,只看參數列表。
7.4 元註解
元註解:元註解的做用就是負責註解其餘註解。Java5.0定義了4個標準的meta-annotation類型,它們被用來提供對其它 annotation類型做說明。
a.@Target:
@Target說明了Annotation所修飾的對象範圍:Annotation可被用於 packages、types(類、接口、枚舉、Annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。
在Annotation類型的聲明中使用了target可更加明晰其修飾的目標。
取值(ElementType)有:
1.CONSTRUCTOR:用於描述構造器
2.FIELD:用於描述域
3.LOCAL_VARIABLE:用於描述局部變量
4.METHOD:用於描述方法
5.PACKAGE:用於描述包
6.PARAMETER:用於描述參數
7.TYPE:用於描述類、接口(包括註解類型) 或enum聲明
看table這個類:註解Table 能夠用於註解類、接口(包括註解類型) 或enum聲明,而註解NoDBColumn僅可用於註解類的成員變量。
b.@Retention:
@Retention定義了該Annotation被保留的時間長短:某些Annotation僅出如今源代碼中,而被編譯器丟棄;而另外一些卻被編譯在class文件中;
編譯在class文件中的Annotation可能會被虛擬機忽略,而另外一些在class被裝載時將被讀取(請注意並不影響class的執行,由於Annotation與class在使用上是被分離的)。
使用這個meta-Annotation能夠對 Annotation的「生命週期」限制。
做用:表示須要在什麼級別保存該註釋信息,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)
取值(RetentionPoicy)有:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在運行時有效(即運行時保留)
看Column這個類:Column註解的的RetentionPolicy的屬性值是RUTIME,這樣註解處理器能夠經過反射,獲取到該註解的屬性值,從而去作一些運行時的邏輯處理
c.@Documented:
@Documented用於描述其它類型的annotation應該被做爲被標註的程序成員的公共API,所以能夠被例如javadoc此類的工具文檔化。
Documented是一個標記註解,沒有成員
咱們能夠在上面那個類中添加@Documented
d.@Inherited:
@Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的類型是被繼承的。
若是一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類。
看Greeting這個類代表這個類型時被繼承的
7.5.Android support annotations
Android support library從19.1版本開始引入了一個新的註解庫,它包含不少有用的元註解,你能用它們修飾你的代碼,幫助你發現bug。
Support library本身自己也用到了這些註解,因此做爲support library的用戶,Android Studio已經基於這些註解校驗了你的代碼而且標註其中潛在的問題。
註解默認是沒有包含的;它被包裝成一個獨立的庫,若是使用了appcompat庫,那麼Support Annotations就會自動引入進來,由於appcompat使用了Support Annotations,若是沒有則須要在build.gradle中添加配置
分類:
1)Nullness註解
@NonNull註解能夠用來標識特定的參數或者返回值不能夠爲null
因爲代碼中參數String s使用@NonNull註解修飾,所以IDE將會以警告的形式提醒咱們這個地方有問題:
若是咱們給name賦值,例如String name = 「Our Lord Duarte」,那麼警告將消失。
使用@Nullable註解修飾的函數參數或者返回值能夠爲null。假設User類有一個名爲name的變量,使用User.getName()訪問,
由於getName函數的返回值使用@Nullable修飾,因此調用:toast的時候沒有檢查getName的返回值是否爲空,將可能致使crash。
2)Resource Type 註解
資源在Android中做爲整型值來傳遞。
這意味着但願獲取一個drawable做爲參數的代碼很容易被傳遞了一個string類型的資源,由於他們資源id都是整型的,編譯器很難區分。
Resource Type註解在這種條件下能夠提供類型檢查
是否曾經傳遞了錯誤的資源整型值給函數,還可以愉快的獲得原本想要的整型值嗎?
資源類型註解能夠幫助咱們準確實現這一點。在下面的代碼中,咱們的testStringRes函數預期接受一個字符串類型的id,並使用@StringRes註解修飾:
3)Threading 註解
好比咱們在項目中處理比較耗時的操做,須要制定在工做子線程中執行,能夠使用Threading 註解,若是沒有在制定的線程中執行也是編譯不過的
幾種Threading註解
@UiThread UI線程
@MainThread 主線程
@WorkerThread 子線程
@BinderThread 綁定線程
4)Overriding Methods
註解: @CallSuper
若是你的API容許使用者重寫你的方法,可是呢,你又須要你本身的方法(父方法)在重寫的時候也被調用,這時候你能夠使用@CallSuper標註
看代碼
7.6.總結
註解是如何被處理的:
當Java源代碼被編譯時,編譯器的一個插件annotation處理器則會處理這些annotation。
處理器能夠產生報告信息,或者建立附加的Java源文件或資源。
若是annotation自己被加上了RententionPolicy的運行時類,則Java編譯器則會將annotation的元數據存儲到class文件中。
而後,Java虛擬機或其餘的程序能夠查找這些元數據並作相應的處理。
固然除了annotation處理器能夠處理annotation外,咱們也能夠使用反射本身來處理annotation
Annotation翻譯爲中文即爲註解,意思就是提供除了程序自己邏輯外的額外的數據信息。
Annotation對於標註的代碼沒有直接的影響,它不能夠直接與標註的代碼產生交互,但其餘組件能夠使用這些信息。
Annotation信息能夠被編譯進class文件,也能夠保留在Java 虛擬機中,從而在運行時能夠獲取。
甚至對於Annotation自己也能夠加Annotation。
8. 類加載器相關
8.1 什麼是ClassLoader?
你們都知道,當咱們寫好一個Java程序以後,不是管是CS仍是BS應用,都是由若干個.class文件組織而成的一個完整的Java應用程序,
當程序在運行時,即會調用該程序的一個入口函數來調用系統的相關功能,而這些功能都被封裝在不一樣的class文件當中,因此常常要從這個class文件中要調用另一個class文件中的方法,若是另一個文件不存在的,則會引起系統異常。
而程序在啓動的時候,並不會一次性加載程序所要用的全部class文件,而是根據程序的須要,經過Java的類加載機制(ClassLoader)來動態加載某個class文件到內存當中的,從而只有class文件被載入到了內存以後,才能被其它class所引用。
因此ClassLoader就是用來動態加載class文件到內存當中用的。
ClassLoader的具體做用就是將class文件加載到jvm虛擬機中去,程序就能夠正確運行了。
可是,jvm啓動的時候,並不會一次性加載全部的class文件,而是根據須要去動態加載。
想一想也是的,一次性加載那麼多jar包那麼多class,那內存不崩潰?
8.2 累加器的類型
- BootStrap ClassLoader:稱爲啓動類加載器,是Java類加載層次中最頂層的類加載器,負責加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等,可經過以下程序得到該類加載器從哪些地方加載了相關的jar或class文件:
- URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
- for (int i = 0; i < urls.length; i++) {
- System.out.println(urls[i].toExternalForm());
- }
如下內容是上述程序從本機JDK環境所得到的結果:
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/classes/
其實上述結果也是經過查找sun.boot.class.path這個系統屬性所得知的。
- System.out.println(System.getProperty("sun.boot.class.path"));
打印結果:C:\Program Files\Java\jdk1.6.0_22\jre\lib\resources.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jce.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.6.0_22\jre\classes
- Extension ClassLoader:稱爲擴展類加載器,負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目下的全部jar。
- App ClassLoader:稱爲系統類加載器,負責加載應用程序classpath目錄下的全部jar和class文件。
啓動(Bootstrap)類加載器
啓動類加載器主要加載的是JVM自身須要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib
路徑下的核心類庫或-Xbootclasspath
參數指定的路徑下的jar包加載到內存中,注意必因爲虛擬機是按照文件名識別加載jar包的,如rt.jar,若是文件名不被虛擬機識別,即便把jar包丟到lib目錄下也是沒有做用的(出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類)。
擴展(Extension)類加載器
擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader
類,由Java語言實現的,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext
目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者能夠直接使用標準擴展類加載器。
系統(System)類加載器
也稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader
。
它負責加載系統類路徑java -classpath
或-D java.class.path
指定路徑下的類庫,也就是咱們常常用到的classpath路徑,開發者能夠直接使用系統類加載器,通常狀況下該類加載是程序中默認的類加載器,經過ClassLoader#getSystemClassLoader()
方法能夠獲取到該類加載器。
在Java的平常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,須要注意的是,Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象,並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式。
8.3 雙親委派代理模式
【雙親委派模型過程】
ClassLoader使用的是雙親委託模型來搜索類的,每一個ClassLoader實例都有一個父類加載器的引用(不是繼承的關係,是一個包含的關係),虛擬機內置的類加載器(Bootstrap ClassLoader)自己沒有父類加載器,但能夠用做其它ClassLoader實例的父類加載器。
當一個ClassLoader實例須要加載某個類時,它會試圖親自搜索某個類以前,先把這個任務委託給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,若是沒加載到,則把任務轉交給Extension ClassLoader試圖加載,若是也沒加載到,則轉交給App ClassLoader 進行加載,若是它也沒有加載獲得的話,則返回給委託的發起者,由它到指定的文件系統或網絡等URL中加載該類。
若是它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。不然將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。
8.4【類的加載過程】
類從被加載到JVM中開始,到卸載爲止,整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。
其中類加載過程包括加載、驗證、準備、解析和初始化五個階段。
加載、驗證、準備、初始化這四個時間是肯定的,
解析的時間是不肯定的,某些狀況支持初始化以後進行,支持java的動態綁定;
使用和卸載是在全部順序定完以後才進行的
程序綁定的概念:
綁定指的是一個方法的調用與方法所在的類(方法主體)關聯起來。對java來講,綁定分爲靜態綁定和動態綁定;或者叫作前期綁定和後期綁定.
靜態綁定:
在程序執行前方法已經被綁定(也就是說在編譯過程當中就已經知道這個方法究竟是哪一個類中的方法),此時由編譯器或其它鏈接程序實現。例如:C。
針對java簡單的能夠理解爲程序編譯期的綁定;這裏特別說明一點,java當中的方法只有final,static,private和構造方法是前期綁定
動態綁定:
後期綁定:在運行時根據具體對象的類型進行綁定。
若一種語言實現了後期綁定,同時必須提供一些機制,可在運行期間判斷對象的類型,並分別調用適當的方法。
也就是說,編譯器此時依然不知道對象的類型,但方法調用機制能本身去調查,找到正確的方法主體。
不一樣的語言對後期綁定的實現方法是有所區別的。
但咱們至少能夠這樣認爲:它們都要在對象中安插某些特殊類型的信息。
1、類加載過程
其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。
另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。
這裏簡要說明下Java中的綁定:綁定指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,對java來講,綁定分爲靜態綁定和動態綁定:
- 靜態綁定:即前期綁定。在程序執行前方法已經被綁定,此時由編譯器或其它鏈接程序實現。針對java,簡單的能夠理解爲程序編譯期的綁定。java當中的方法只有final,static,private和構造方法是前期綁定的。
- 動態綁定:即晚期綁定,也叫運行時綁定。在運行時根據具體對象的類型進行綁定。在java中,幾乎全部的方法都是後期綁定的。
下面詳細講述類加載過程當中每一個階段所作的工做
加載
加載時類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:
一、經過一個類的全限定名來獲取其定義的二進制字節流。
二、將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
三、在Java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。
注意,這裏第1條中的二進制字節流並不僅是單純地從Class文件中獲取,好比它還能夠從Jar包中獲取、從網絡中獲取(最典型的應用即是Applet)、由其餘文件生成(JSP應用)等。
相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既能夠使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。
加載階段完成後,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。
驗證
驗證的目的是爲了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
不一樣的虛擬機對類驗證的實現可能會有所不一樣,但大體都會完成如下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
- 文件格式的驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內。通過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。
- 元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規範的元數據信息。
- 字節碼驗證:該階段驗證的主要工做是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會作出危害虛擬機安全的行爲。
- 符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。
準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:
一、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。
二、這裏所設置的初始值一般狀況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設一個類變量的定義爲:
public static int value = 3;
那麼變量value在準備階段事後的初始值爲0,而不是3,由於這時候還沒有開始執行任何Java方法,而把value賦值爲3的putstatic指令是在程序編譯後,存放於類構造器<clinit>()方法之中的,因此把value賦值爲3的動做將在初始化階段纔會執行。
下表列出了Java中全部基本數據類型以及reference類型的默認零值:
這裏還須要注意以下幾點:
- 對基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過。
- 對於同時被static和final修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過;而只被final修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。
- 對於引用數據類型reference來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。
- 若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。
解析
解析階段是虛擬機將
常量池中的符號引用轉化爲直接引用的過程。在
Class類文件結構一文中已經比較過了符號引用和直接引用的區別和關聯,這裏再也不贅述。
前面說解析階段可能開始於初始化以前,也可能在初始化以後開始,虛擬機會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析(初始化以前),仍是等到一個符號引用將要被使用前纔去解析它(初始化以後)。
對同一個符號引用進行屢次解析請求時很常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標示爲已解析狀態),從而避免解析動做重複進行。
解析動做主要針對
類或接口、字段、類方法、接口方法四類符號引用進行,分別對應於常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量類型。
一、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,仍是普通的對象類型的引用,從而進行不一樣的解析。
二、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,若是有,則查找結束;
若是沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,尚未,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束,查找流程以下圖所示:
三、類方法解析:對類方法的解析與對字段解析的搜索步驟差很少,只是多了判斷該方法所處的是類仍是接口的步驟,並且對類方法的匹配搜索,是先
搜索父類,再搜索接口。
四、接口方法解析:與類方法解析步驟相似,只是接口不會有父類,所以,只遞歸向上
搜索父接口就好了。
初始化
初始化是類加載過程的最後一步,到了此階段,才
真正開始執行類中定義的Java程序代碼。
在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序指定的主觀計劃去初始化類變量和其餘資源,或者能夠從另外一個角度來表達:
初始化階段是執行類構造器<clinit>()方法的過程。
這裏簡單說明下<clinit>()方法的執行規則:
一、<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句中能夠賦值,可是不能訪問。
二、<clinit>()方法與實例構造器<init>()方法(類的構造函數)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以,在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object。
三、<clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。
四、接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操做,所以接口與類同樣會生成<clinit>()方法。可是接口魚類不一樣的是:執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
五、虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。
下面給出一個簡單的例子,以便更清晰地說明如上規則:
- class Father{
- public static int a = 1;
- static{
- a = 2;
- }
- }
-
- class Child extends Father{
- public static int b = a;
- }
-
- public class ClinitTest{
- public static void main(String[] args){
- System.out.println(Child.b);
- }
- }
執行上面的代碼,會打印出2,也就是說b的值被賦爲了2。
咱們來看獲得該結果的步驟。首先在準備階段爲類變量分配內存並設置類變量初始值,這樣A和B均被賦值爲默認值0,然後再在調用<clinit>()方法時給他們賦予程序中指定的值。當咱們調用Child.b時,觸發Child的<clinit>()方法,根據規則2,在此以前,要先執行完其父類Father的<clinit>()方法,又根據規則1,在執行<clinit>()方法時,須要按static語句或static變量賦值操做等在代碼中出現的順序來執行相關的static語句,所以當觸發執行Father的<clinit>()方法時,會先將a賦值爲1,再執行static語句塊中語句,將a賦值爲2,然後再執行Child類的<clinit>()方法,這樣便會將b的賦值爲2.
若是咱們顛倒一下Father類中「public static int a = 1;」語句和「static語句塊」的順序,程序執行後,則會打印出1。很明顯是根據規則1,執行Father的<clinit>()方法時,根據順序先執行了static語句塊中的內容,後執行了「public static int a = 1;」語句。
另外,在顛倒兩者的順序以後,若是在static語句塊中對a進行訪問(好比將a賦給某個變量),在編譯時將會報錯,由於根據規則1,它只能對a進行賦值,而不能訪問。
總結
整個類加載過程當中,除了在加載階段用戶應用程序能夠自定義類加載器參與以外,其他
全部的動做徹底由虛擬機主導和控制。到了初始化纔開始執行類中定義的Java程序代碼(亦及字節碼),但這裏的執行代碼只是個開端,它僅限於<clinit>()方法。類加載過程當中主要是將Class文件(準確地講,應該是類的二進制字節流)加載到虛擬機內存中,真正執行字節碼的操做,在加載完成後才真正開始。
8.5 自定義類加載器
Java類加載機制及自定義加載器
一:ClassLoader類加載器,主要的做用是將class文件加載到jvm虛擬機中。jvm啓動的時候,並非一次性加載全部的類,而是根據須要動態去加載類,主要分爲隱式加載和顯示加載。
隱式加載:程序代碼中不經過調用ClassLoader來加載須要的類,而是經過JVM類自動加載須要的類到內存中。例如,當咱們在類中繼承或者引用某個類的時候,JVM在解析當前這個類的時,發現引用的類不在內存中,那麼就會自動將這些類加載到內存中。
顯示加載:代碼中經過Class.forName(),this.getClass.getClassLoader.LoadClass(),自定義類加載器中的findClass()方法等。
二:jvm自帶的加載器
(1)BootStrap ClassLoader:主要加載%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。能夠通System.getProperty("sun.boot.class.path")
查看加載的路徑,以下:
複製代碼
package test;
public class TestGC {
public static void main(String []args){
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
複製代碼
顯示結果以下:
D:\Program Files\Java\jdk1.7.0_45\jre\lib\resources.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\rt.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\sunrsasign.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\jsse.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\jce.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\charsets.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\jfr.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\classes
(2)Extention ClassLoader:主要加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。也能夠經過System.out.println(System.getProperty("java.ext.dirs"))查看加載類文件的路徑。
(3)AppClassLoader:主要加載當前應用下的classpath路徑下的類。以前咱們在環境變量中配置的classpath就是指定AppClassLoader的類加載路徑。
三:類加載器的繼承關係
先看一下這三個類加載器之間的繼承關係,以下圖:
ExtClassLoader,AppClassLoder繼承URLClassLoader,而URLClassLoader繼承ClassLoader,BoopStrap ClassLoder不在上圖中,由於它是由C/C++編寫的,它自己是虛擬機的一部分,並非一個java類。
jvm加載的順序:BoopStrap ClassLoder-〉ExtClassLoader->AppClassLoder,下面看一段源碼:
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
從源碼中咱們看到:
(1)Launcher初始化的時候建立了ExtClassLoader以及AppClassLoader,並將ExtClassLoader實例傳入到AppClassLoader中。
(2)雖然上一段源碼中沒見到建立BoopStrap ClassLoader,可是程序一開始就執行了System.getProperty("sun.boot.class.path")。
四:類加載器之間的父子關係
AppClassLoader的父加載器爲ExtClassLoader,ExtClassLoader的父加載器爲null,BoopStrap ClassLoader爲頂級加載器。
下面一個小例子就能夠證實,以下:新建一個Test類,能夠經過getParent()方法獲取上一層父機載器,執行以下代碼:
複製代碼
package test;
public class TestGC {
public static void main(String []args){
System.out.println(Test.class.getClassLoader().toString());
System.out.println(Test.class.getClassLoader().getParent().toString());
System.out.println(Test.class.getClassLoader().getParent().getParent().toString());
}
}
複製代碼
輸出結果以下:
五:類加載機制-雙親委託機制
例如:當jvm要加載Test.class的時候,
(1)首先會到自定義加載器中查找,看是否已經加載過,若是已經加載過,則返回字節碼。
(2)若是自定義加載器沒有加載過,則詢問上一層加載器(即AppClassLoader)是否已經加載過Test.class。
(3)若是沒有加載過,則詢問上一層加載器(ExtClassLoader)是否已經加載過。
(4)若是沒有加載過,則繼續詢問上一層加載(BoopStrap ClassLoader)是否已經加載過。
(5)若是BoopStrap ClassLoader依然沒有加載過,則到本身指定類加載路徑下("sun.boot.class.path")查看是否有Test.class字節碼,有則返回,沒有通
知下一層加載器ExtClassLoader到本身指定的類加載路徑下(java.ext.dirs)查看。
(6)依次類推,最後到自定義類加載器指定的路徑尚未找到Test.class字節碼,則拋出異常ClassNotFoundException。以下圖:
六:類加載過程的幾個方法
(1)loadClass
(2)findLoadedClass
(3)findClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,檢查是否已經加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加載器不爲空,調用父加載器的loadClass
c = parent.loadClass(name, false);
} else {
//父加載器爲空則,調用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加載器沒有找到,則調用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//調用resolveClass()
resolveClass(c);
}
return c;
}
}
七:自定義類加載器步驟
(1)繼承ClassLoader
(2)重寫findClass()方法
(3)調用defineClass()方法
下面寫一個自定義類加載器:指定類加載路徑在D盤下的lib文件夾下。
(1)新建一個Test.class類,代碼以下:
package com.test;
public class Test {
public void say(){
System.out.println("Hello MyClassLoader");
}
}
(2)cmd控制檯執行javac Test.java,將生成的Test.class文件放到D盤lib文件夾->com文件夾->test文件夾下。
(3)自定義類加載器,代碼以下:
package test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
private String classpath;
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte [] classDate=getDate(name);
if(classDate==null){}
else{
//defineClass方法將字節碼轉化爲類
return defineClass(name,classDate,0,classDate.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
//返回類的字節碼
private byte[] getDate(String className) throws IOException{
InputStream in = null;
ByteArrayOutputStream out = null;
String path=classpath + File.separatorChar +
className.replace('.',File.separatorChar)+".class";
try {
in=new FileInputStream(path);
out=new ByteArrayOutputStream();
byte[] buffer=new byte[2048];
int len=0;
while((len=in.read(buffer))!=-1){
out.write(buffer,0,len);
}
return out.toByteArray();
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
finally{
in.close();
out.close();
}
return null;
}
測試代碼以下:
package test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestMyClassLoader {
public static void main(String []args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException{
//自定義類加載器的加載路徑
MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");
//包名+類名
Class c=myClassLoader.loadClass("com.test.Test");
if(c!=null){
Object obj=c.newInstance();
Method method=c.getMethod("say", null);
method.invoke(obj, null);
System.out.println(c.getClassLoader().toString());
}
}
}
複製代碼
輸出結果以下:
自定義類加載器的做用:jvm自帶的三個加載器只能加載指定路徑下的類字節碼。
若是某個狀況下,咱們須要加載應用程序以外的類文件呢?好比本地D盤下的,或者去加載網絡上的某個類文件,這種狀況就能夠使用自定義加載器了。
參考網址:http://blog.csdn.net/briblue/article/details/54973413
9.反射
9.1 編譯時運行時的概念
【說明】二者最大的不一樣是:是否涉及到內存的調用;
【編譯時】只涉及糾正內存的語法正確與否,不涉及內存運行;
【運行時】java虛擬機執行.class文件的過程,涉及到內存的調用;
編譯期,就是將Java代碼編譯成.class文件的過程,該過程只涉及到語法句法的正確與否,不涉及內存方面及執行方面的檢查。
所謂的運行期,就是Java虛擬機執行.class文件的過程。該過程會涉及到內存調用。實際類型檢查等方面。
關於動態綁定,在調用該引用實例的方法的時候,會優先去調用該實例引用的運行時方法,也就是實際類型的方法。
而在調用該引用實例的成員變量的時候,會優先去調用該實例應用的編譯時的成員變量,也就是聲明的類型的成員變量。
對於,調用引用實例的方法,在編譯時,是調用聲明類型的成員方法(多態的實現原理),也就是所謂的編譯時類型的方法,而到了運行時,調用的是實際的類型的成員方法,也就是所謂的運行時類型的方法。
而對於調用引用實例的成員變量,在編譯時,便是調用聲明類型的成員變量,在運行時更是調用聲明類型的成員變量,
也就是說,對於調用引用實例的成員變量,不管是編譯時仍是運行時,均是調用編譯時類型的成員變量。
9.2 什麼是反射
JAVA語言的反射機制:
JAVA反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;
對於任意一個對象,都可以調用它的任意一個方法和屬性;
這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。
在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;
對於任意一個對象,都可以調用它的任意一個方法;
這 種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。
主要功能:
在運行時判斷任意一個對象所屬的類;
在運行時構造任意一個類的對象;
在運行時判斷任意一個類所具備的成員變量和方法;
在運行時調用任意一個對象的方法;
生成動態代理。
9.3.class文件
jvm虛擬機會加載.class文件,.class文件是由編譯器將java源代碼編譯成.class文件的。
每個.class文件都有一個class對象,這裏的class和.class文件是不同的。
編譯好一個類class是生成在.class文件中的;
生一個.class文件就會生成一個class對象,是記錄class對象的信息的;
9.4 反射的應用
9.5 android 中反射的應用
【說明】若是xml佈局文件、r文件的使用等等;