Java面試題集(第二部分)(51-70)

摘要:這一部分主要講解了異常、多線程、容器和I/O的相關面試題。首先,異常機制提供了一種在不打亂原有業務邏輯的前提下,把程序在運行時可能出現的狀況處理掉的優雅的解決方案,同時也是面向對象的解決方案。而Java的線程模型是建立在共享的、默認的可見的可變狀態以及搶佔式線程調度兩個概念之上的。Java內置了對多線程編程的支持在20世紀90年代可以說是一個巨大的進步,但是最初的設計在當下看來已經給程序帶來很多困擾了。感謝Doug Lea在Java 5中提供了他里程碑式的傑作java.util.concurrent包,它的出現讓Java的多線程編程能夠更好的工作。Java 1.4中引入NIO實現了對非阻塞I/O的支持,NIO爲I/O操作抽象出緩衝區和通道層,解決了字符集的編碼和解碼問題,提供了將文件映射爲內存數據的接口。NIO無疑使Java向前邁出了一大步,但爲了方便Java對文件系統的處理,NIO.2進一步對Java的I/O操作進行了增強,提供了能批量獲取文件屬性的文件系統接口,還提供了套接字和文件都能進行異步IO操作的API,完成了JSR-51中定義的套接字。對於Java中的容器(集合框架)而言,java 5中引入泛型無疑是程序員的福音,然而那僅僅是糖衣語法,底層實現沒有本質的差別,因此與C#相比,Java的泛型顯得不那麼讓人痛快。


51、類ExampleA 繼承Exception,類ExampleB 繼承ExampleA。

有如下代碼片斷:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. try{  
  2.     throw new ExampleB(“b”)  
  3. }catch(ExampleA e){  
  4.     System.out.println(”ExampleA”);  
  5. }catch(Exception e){  
  6.     System.out.println(”Exception”);  
  7. }  
請問執行此段代碼的輸出是什麼?

答:輸出:ExampleA。(根據里氏代換原則[能使用父類型的地方一定能使用子類型],抓取ExampleA類型異常的catch塊能夠抓住try塊中拋出的ExampleB類型的異常)

補充:比此題略複雜的一道面試題如下所示(此題的出處是《Java編程思想》),說出你的答案吧!

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. class Annoyance extends Exception {}  
  2. class Sneeze extends Annoyance {}  
  3.   
  4. class Human {  
  5.   
  6.     public static void main(String[] args)   
  7.         throws Exception {  
  8.         try {  
  9.             try {  
  10.                 throw new Sneeze();  
  11.             }   
  12.             catch ( Annoyance a ) {  
  13.                 System.out.println(”Caught Annoyance”);  
  14.                 throw a;  
  15.             }  
  16.         }   
  17.         catch ( Sneeze s ) {  
  18.             System.out.println(”Caught Sneeze”);  
  19.             return ;  
  20.         }  
  21.         finally {  
  22.             System.out.println(”Hello World!”);  
  23.         }  
  24.     }  
  25. }  

 52、List、Set、Map 是否繼承自Collection 接口?

答:List、Set 是,Map 不是。Map是鍵值對映射容器,與List和Set有明顯的區別,而Set存儲的零散的元素且不允許有重複元素(數學中的集合也是如此),List是線性結構的容器,適用於按數值索引訪問元素的情形。

 

53、說出ArrayList、Vector、LinkedList 的存儲性能和特性?

答:ArrayList 和Vector都是使用數組方式存儲數據,此數組元素數大於實際存儲的數據以便增加和插入元素,它們都允許直接按序號索引元素,但是插入元素要涉及數組元素移動等內存操作,所以索引數據快而插入數據慢,Vector由於使用了synchronized 方法(線程安全),通常性能上較ArrayList 差,而LinkedList 使用雙向鏈表實現存儲(將內存中零散的內存單元通過附加的引用關聯起來,形成一個可以按序號索引的線性結構,這種鏈式存儲方式與數組的連續存儲方式相比,其實對內存的利用率更高),按序號索引數據需要進行前向或後向遍歷,但是插入數據時只需要記錄本項的前後項即可,所以插入速度較快。Vector屬於遺留容器(早期的JDK中使用的容器,除此之外Hashtable、Dictionary、BitSet、Stack、Properties都是遺留容器),現在已經不推薦使用,但是由於ArrayList和LinkedListed都是非線程安全的,如果需要多個線程操作同一個容器,那麼可以通過工具類Collections中的synchronizedList方法將其轉換成線程安全的容器後再使用(這其實是裝潢模式最好的例子,將已有對象傳入另一個類的構造器中創建新的對象來增加新功能)。

補充:遺留容器中的Properties類和Stack類在設計上有嚴重的問題,Properties是一個鍵和值都是字符串的特殊的鍵值對映射,在設計上應該是關聯一個Hashtable並將其兩個泛型參數設置爲String類型,但是Java API中的Properties直接繼承了Hashtable,這很明顯是對繼承的濫用。這裏複用代碼的方式應該是HAS-A關係而不是IS-A關係,另一方面容器都屬於工具類,繼承工具類本身就是一個錯誤的做法,使用工具類最好的方式是HAS-A關係(關聯)或USE-A關係(依賴)。同理,Stack類繼承Vector也是不正確的。

 

54、Collection 和Collections 的區別?

答:Collection 是一個接口,它是Set、List等容器的父接口;Collections 是個一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜索、排序、線程安全化等等。

 

55、List、Map、Set 三個接口,存取元素時,各有什麼特點?

答:List以特定索引來存取元素,可有重複元素。Set不能存放重複元素(用對象的equals()方法來區分元素是否重複)。Map保存鍵值對(key-value pair)映射,映射關係可以是一對一或多對一。Set和Map容器都有基於哈希存儲和排序樹的兩種實現版本,基於哈希存儲的版本理論存取時間複雜度爲O(1),而基於排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。

 

56、TreeMap和TreeSet在排序時如何比較元素?Collections工具類中的sort()方法如何比較元素?

答:TreeSet要求存放的對象所屬的類必須實現Comparable接口,該接口提供了比較元素的compareTo()方法,當插入元素時會回調該方法比較元素的大小。TreeMap要求存放的鍵值對映射的鍵必須實現Comparable接口從而根據鍵對元素進行排序。Collections工具類的sort方法有兩種重載的形式,第一種要求傳入的待排序容器中存放的對象比較實現Comparable接口以實現元素的比較;第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個參數,參數是Comparator接口的子類型(需要重寫compare方法實現元素的比較),相當於一個臨時定義的排序規則,其實就是是通過接口注入比較元素大小的算法,也是對回調模式的應用。

 例子1:

Student.java

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.lovo.demo;  
  2.   
  3. public class Student implements Comparable<Student> {  
  4.     private String name;        // 姓名  
  5.     private int age;            // 年齡  
  6.   
  7.     public Student(String name, int age) {  
  8.         this.name = name;  
  9.         this.age = age;  
  10.     }  
  11.   
  12.     @Override  
  13.     public String toString() {  
  14.         return “Student [name=” + name + “, age=” + age + “]”;  
  15.     }  
  16.   
  17.     @Override  
  18.     public int compareTo(Student o) {  
  19.         return this.age - o.age; // 比較年齡(年齡的升序)  
  20.     }  
  21.   
  22. }  
Test01.java
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.lovo.demo;  
  2.   
  3. import java.util.Set;  
  4. import java.util.TreeSet;  
  5.   
  6. class Test01 {  
  7.   
  8.     public static void main(String[] args) {  
  9.         Set<Student> set = new TreeSet<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫類型)  
  10.         set.add(new Student(“Hao LUO”33));  
  11.         set.add(new Student(“XJ WANG”32));  
  12.         set.add(new Student(“Bruce LEE”60));  
  13.         set.add(new Student(“Bob YANG”22));  
  14.           
  15.         for(Student stu : set) {  
  16.             System.out.println(stu);  
  17.         }  
  18. //      輸出結果:   
  19. //      Student [name=Bob YANG, age=22]  
  20. //      Student [name=XJ WANG, age=32]  
  21. //      Student [name=Hao LUO, age=33]  
  22. //      Student [name=Bruce LEE, age=60]  
  23.     }  
  24. }  

例子2:

Student.java

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.lovo.demo;  
  2.   
  3. public class Student {  
  4.     private String name;    // 姓名  
  5.     private int age;        // 年齡  
  6.   
  7.     public Student(String name, int age) {  
  8.         this.name = name;  
  9.         this.age = age;  
  10.     }  
  11.   
  12.     /** 
  13.      * 獲取學生姓名 
  14.      */  
  15.     public String getName() {  
  16.         return name;  
  17.     }  
  18.   
  19.     /** 
  20.      * 獲取學生年齡 
  21.      */  
  22.     public int getAge() {  
  23.         return age;  
  24.     }  
  25.   
  26.     @Override  
  27.     public String toString() {  
  28.         return “Student [name=” + name + “, age=” + age + “]”;  
  29.     }  
  30.   
  31. }  
Test02.java

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.lovo.demo;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.Collections;  
  5. import java.util.Comparator;  
  6. import java.util.List;  
  7.   
  8. class Test02 {  
  9.   
  10.     public static void main(String[] args) {  
  11.         List<Student> list = new ArrayList<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫類型)  
  12.         list.add(new Student(“Hao LUO”33));  
  13.         list.add(new Student(“XJ WANG”32));  
  14.         list.add(new Student(“Bruce LEE”60));  
  15.         list.add(new Student(“Bob YANG”22));  
  16.           
  17.         // 通過sort方法的第二個參數傳入一個Comparator接口對象  
  18.         // 相當於是傳入一個比較對象大小的算法到sort方法中  
  19.         // 由於Java中沒有函數指針、仿函數、委託這樣的概念  
  20.         // 因此要將一個算法傳入一個方法中唯一的選擇就是通過接口回調  
  21.         Collections.sort(list, new Comparator<Student> () {  
  22.   
  23.             @Override  
  24.             public int compare(Student o1, Student o2) {  
  25.                 return o1.getName().compareTo(o2.getName());    // 比較學生姓名  
  26.             }  
  27.         });  
  28.           
  29.         for(Student stu : list) {  
  30.             System.out.println(stu);  
  31.         }  
  32. //      輸出結果:   
  33. //      Student [name=Bob YANG, age=22]  
  34. //      Student [name=Bruce LEE, age=60]  
  35. //      Student [name=Hao LUO, age=33]  
  36. //      Student [name=XJ WANG, age=32]  
  37.     }  
  38. }  


57、sleep()和wait()有什麼區別?

答:sleep()方法是線程類(Thread)的靜態方法,導致此線程暫停執行指定時間,將執行機會給其他線程,但是監控狀態依然保持,到時後會自動恢復(線程回到就緒(ready)狀態),因爲調用sleep 不會釋放對象鎖。wait()是Object 類的方法,對此對象調用wait()方法導致本線程放棄對象鎖(線程暫停執行),進入等待此對象的等待鎖定池,只有針對此對象發出notify 方法(或notifyAll)後本線程才進入對象鎖定池準備獲得對象鎖進入就緒狀態。

補充:這裏似乎漏掉了一個作爲先決條件的問題,就是什麼是進程,什麼是線程?爲什麼需要多線程編程?答案如下所示:

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,是操作系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是CPU調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時通常擁有獨立的內存單元,而線程之間可以共享內存。使用多線程的編程通常能夠帶來更好的性能和用戶體驗,但是多線程的程序對於其他程序是不友好的,因爲它佔用了更多的CPU資源。

 

58、sleep()和yield()有什麼區別?

答:

① sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;

② 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;

③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;

④ sleep()方法比yield()方法(跟操作系統相關)具有更好的可移植性。

 

59、當一個線程進入一個對象的synchronized方法A之後,其它線程是否可進入此對象的synchronized方法?

答:不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。

 

60、請說出與線程同步相關的方法。

答:

  1. wait():使一個線程處於等待(阻塞)狀態,並且釋放所持有的對象的鎖;
  2. sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要捕捉InterruptedException 異常;
  3. notify():喚醒一個處於等待狀態的線程,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且與優先級無關;
  4. notityAll():喚醒所有處入等待狀態的線程,注意並不是給所有喚醒線程一個對象的鎖,而是讓它們競爭;
  5. JDK 1.5通過Lock接口提供了顯式(explicit)的鎖機制,增強了靈活性以及對線程的協調。Lock接口中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了newCondition()方法來產生用於線程之間通信的Condition對象;
  6. JDK 1.5還提供了信號量(semaphore)機制,信號量可以用來限制對某個共享資源進行訪問的線程的數量。在對資源進行訪問之前,線程必須得到信號量的許可(調用Semaphore對象的acquire()方法);在完成對資源的訪問後,線程必須向信號量歸還許可(調用Semaphore對象的release()方法)。
下面的例子演示了100個線程同時向一個銀行賬戶中存入1元錢,在沒有使用同步機制和使用同步機制情況下的執行情況。
銀行賬戶類:
[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 銀行賬戶 
  5.  * 
  6.  */  
  7. public class Account {  
  8.     private double balance;     // 賬戶餘額  
  9.       
  10.     /** 
  11.      * 存款 
  12.      * @param money 存入金額 
  13.      */  
  14.     public void deposit(double money) {  
  15.         double newBalance = balance + money;  
  16.         try {  
  17.             Thread.sleep(10);   // 模擬此業務需要一段處理時間  
  18.         }  
  19.         catch(InterruptedException ex) {  
  20.             ex.printStackTrace();  
  21.         }  
  22.         balance = newBalance;  
  23.     }  
  24.       
  25.     /** 
  26.      * 獲得賬戶餘額 
  27.      */  
  28.     public double getBalance() {  
  29.         return balance;  
  30.     }  
  31. }  
存錢線程類:
[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 存錢線程 
  5.  * 
  6.  */  
  7. public class AddMoneyThread implements Runnable {  
  8.     private Account account;    // 存入賬戶  
  9.     private double money;       // 存入金額  
  10.   
  11.     public AddMoneyThread(Account account, double money) {  
  12.         this.account = account;  
  13.         this.money = money;  
  14.     }  
  15.   
  16.     @Override  
  17.     public void run() {  
  18.         account.deposit(money);  
  19.     }  
  20.   
  21. }  
測試類:
[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. import java.util.concurrent.ExecutorService;  
  4. import java.util.concurrent.Executors;  
  5.   
  6. public class Test01 {  
  7.   
  8.     public static void main(String[] args) {  
  9.         Account account = new Account();  
  10.         ExecutorService service = Executors.newFixedThreadPool(100);  
  11.           
  12.         for(int i = 1; i <= 100; i++) {  
  13.             service.execute(new AddMoneyThread(account, 1));  
  14.         }  
  15.           
  16.         service.shutdown();  
  17.           
  18.         while(!service.isTerminated()) {}  
  19.           
  20.         System.out.println(”賬戶餘額: ” + account.getBalance());  
  21.     }  
  22. }  
在沒有同步的情況下,執行結果通常是顯示賬戶餘額在10元以下,出現這種狀況的原因是,當一個線程A試圖存入1元的時候,另外一個線程B也能夠進入存款的方法中,線程B讀取到的賬戶餘額仍然是線程A存入1元錢之前的賬戶餘額,因此也是在原來的餘額0上面做了加1元的操作,同理線程C也會做類似的事情,所以最後100個線程執行結束時,本來期望賬戶餘額爲100元,但實際得到的通常在10元以下。解決這個問題的辦法就是同步,當一個線程對銀行賬戶存錢時,需要將此賬戶鎖定,待其操作完成後才允許其他的線程進行操作,代碼有如下幾種調整方案:
1. 在銀行賬戶的存款(deposit)方法上同步(synchronized)關鍵字
[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 銀行賬戶 
  5.  * 
  6.  */  
  7. public class Account {  
  8.     private double balance;     // 賬戶餘額  
  9.       
  10.     /** 
  11.      * 存款 
  12.      * @param money 存入金額 
  13.      */  
  14.     public synchronized void deposit(double money) {  
  15.         double newBalance = balance + money;  
  16.         try {  
  17.             Thread.sleep(10);   // 模擬此業務需要一段處理時間  
  18.         }  
  19.         catch(InterruptedException ex) {  
  20.             ex.printStackTrace();  
  21.         }  
  22.         balance = newBalance;  
  23.     }  
  24.       
  25.     /** 
  26.      * 獲得賬戶餘額 
  27.      */  
  28.     public double getBalance() {  
  29.         return balance;  
  30.     }  
  31. }  
2. 在線程調用存款方法時對銀行賬戶進行同步
[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. /** 
  4.  * 存錢線程 
  5.  * 
  6.  */  
  7. public class AddMoneyThread implements Runnable {  
  8.     private Account account;    // 存入賬戶  
  9.     private double money;       // 存入金額  
  10.   
  11.     public AddMoneyThread(Account account, double money) {  
  12.         this.account = account;  
  13.         this.money = money;  
  14.     }  
  15.   
  16.     @Override  
  17.     public void run() {  
  18.         synchronized (account) {  
  19.             account.deposit(money);   
  20.         }  
  21.     }  
  22.   
  23. }  
3. 通過JDK 1.5顯示的鎖機制,爲每個銀行賬戶創建一個鎖對象,在存款操作進行加鎖和解鎖的操作
[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. import java.util.concurrent.locks.Lock;  
  4. import java.util.concurrent.locks.ReentrantLock;  
  5.   
  6. /** 
  7.  * 銀行賬戶 
  8.  *  
  9.  * 
  10.  */  
  11. public class Account {  
  12.     private Lock accountLock = new ReentrantLock();  
  13.     private double balance; // 賬戶餘額  
  14.   
  15.     /** 
  16.      * 存款 
  17.      *  
  18.      * @param money 
  19.      *            存入金額 
  20.      */  
  21.     public void deposit(double money) {  
  22.         accountLock.lock();  
  23.         try {  
  24.             double newBalance = balance + money;  
  25.             try {  
  26.                 Thread.sleep(10); // 模擬此業務需要一段處理時間  
  27.             }  
  28.             catch (InterruptedException ex) {  
  29.                 ex.printStackTrace();  
  30.             }  
  31.             balance = newBalance;  
  32.         }  
  33.         finally {  
  34.             accountLock.unlock();  
  35.         }  
  36.     }  
  37.   
  38.     /** 
  39.      * 獲得賬戶餘額 
  40.      */  
  41.     public double getBalance() {  
  42.         return balance;  
  43.     }  
  44. }  
按照上述三種方式對代碼進行修改後,重寫執行測試代碼Test01,將看到最終的賬戶餘額爲100元。
 

61、編寫多線程程序有幾種實現方式?

答:Java 5以前實現多線程有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable接口。兩種方式都要通過重寫run()方法來定義線程的行爲,推薦使用後者,因爲Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable接口更爲靈活。

補充:Java 5以後創建線程還有第三種方式:實現Callable接口,該接口中的call方法可以在線程執行結束時產生一個返回值,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.lovo.demo;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5. import java.util.concurrent.Callable;  
  6. import java.util.concurrent.ExecutorService;  
  7. import java.util.concurrent.Executors;  
  8. import java.util.concurrent.Future;  
  9.   
  10.   
  11. class MyTask implements Callable<Integer> {  
  12.     private int upperBounds;  
  13.       
  14.     public MyTask(int upperBounds) {  
  15.         this.upperBounds = upperBounds;  
  16.     }  
  17.       
  18.     @Override  
  19.     public Integer call() throws Exception {  
  20.         int sum = 0;   
  21.         for(int i = 1; i <= upperBounds; i++) {  
  22.             sum += i;  
  23.         }  
  24.         return sum;  
  25.     }  
  26.       
  27. }  
  28.   
  29. public class Test {  
  30.   
  31.     public static void main(String[] args) throws Exception {  
  32.         List<Future<Integer>> list = new ArrayList<>();  
  33.         ExecutorService service = Executors.newFixedThreadPool(10);  
  34.         for(int i = 0; i < 10; i++) {  
  35.             list.add(service.submit(new MyTask((int) (Math.random() * 100))));  
  36.         }  
  37.           
  38.         int sum = 0;  
  39.         for(Future<Integer> future : list) {  
  40.             while(!future.isDone()) ;  
  41.             sum += future.get();  
  42.         }  
  43.           
  44.         System.out.println(sum);  
  45.     }  
  46. }  
 

62、synchronized關鍵字的用法?

答:synchronized關鍵字可以將對象或者方法標記爲同步,以實現對對象和方法的互斥訪問,可以用synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時將synchronized作爲方法的修飾符。在第60題的例子中已經展示了synchronized關鍵字的用法。

 

63、舉例說明同步和異步。

答:如果系統中存在臨界資源(資源數量少於競爭資源的線程數量的資源),例如正在寫的數據以後可能被另一個線程讀到,或者正在讀的數據可能已經被另一個線程寫過了,那麼這些數據就必須進行同步存取(數據庫操作中的悲觀鎖就是最好的例子)。當應用程序在對象上調用了一個需要花費很長時間來執行的方法,並且不希望讓程序等待方法的返回時,就應該使用異步編程,在很多情況下采用異步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而異步就是非阻塞式操作。

 

64、啓動一個線程是用run()還是start()方法?

答:啓動一個線程是調用start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由JVM 調度並執行,這並不意味着線程就會立即運行。run()方法是線程啓動後要進行回調(callback)的方法。

 

65、什麼是線程池(thread pool)?

答:在面向對象編程中,創建和銷燬對象是很費時間的,因爲創建一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷燬後進行垃圾回收。所以提高服務程序效率的一個手段就是儘可能減少創建和銷燬對象的次數,特別是一些很耗資源的對象創建和銷燬,這就是”池化資源”技術產生的原因。線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。

Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態工廠方法,生成一些常用的線程池,如下所示:

  • newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。
  • newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
  • newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
  • newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。
第60題的例子中有通過Executors工具類創建線程池並使用線程池執行線程的代碼。如果希望在服務器上使用線程池,強烈建議使用newFixedThreadPool方法來創建線程池,這樣能獲得更好的性能

 

66、線程的基本狀態以及狀態之間的關係?

答:

 

除去起始(new)狀態和結束(finished)狀態,線程有三種狀態,分別是:就緒(ready)、運行(running)和阻塞(blocked)。其中就緒狀態代表線程具備了運行的所有條件,只等待CPU調度(萬事俱備,只欠東風);處於運行狀態的線程可能因爲CPU調度(時間片用完了)的原因回到就緒狀態,也有可能因爲調用了線程的yield方法回到就緒狀態,此時線程不會釋放它佔有的資源的鎖,坐等CPU以繼續執行;運行狀態的線程可能因爲I/O中斷、線程休眠、調用了對象的wait方法而進入阻塞狀態(有的地方也稱之爲等待狀態);而進入阻塞狀態的線程會因爲休眠結束、調用了對象的notify方法或notifyAll方法或其他線程執行結束而進入就緒狀態。注意:調用wait方法會讓線程進入等待池中等待被喚醒,notify方法或notifyAll方法會讓等待鎖中的線程從等待池進入等鎖池,在沒有得到對象的鎖之前,線程仍然無法獲得CPU的調度和執行。


67、簡述synchronized 和java.util.concurrent.locks.Lock的異同?

答:Lock是Java 5以後引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的所有功能;主要不同點:Lock 有比synchronized 更精確的線程語義和更好的性能。synchronized 會自動釋放鎖,而Lock 一定要求程序員手工釋放,並且必須在finally 塊中釋放(這是釋放外部資源的最好的地方)。

 

68、Java中如何實現序列化,有什麼意義?

答:序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流化。可以對流化後的對象進行讀寫操作,也可將流化後的對象傳輸於網絡之間。序列化是爲了解決對象流讀寫操作時可能引發的問題(如果不進行序列化可能會存在數據亂序的問題)。

要實現序列化,需要讓一個類實現Serializable接口,該接口是一個標識性接口,標註該類對象是可被序列化的,然後使用一個輸出流來構造一個對象輸出流並通過writeObject(Object obj)方法就可以將實現對象寫出(即保存其狀態);如果需要反序列化則可以用一個輸入流建立對象輸入流,然後通過readObject方法從流中讀取對象。序列化除了能夠實現對象的持久化之外,還能夠用於對象的深度克隆(參見Java面試題集1-29題)

 

69、Java 中有幾種類型的流?

答:字節流,字符流。字節流繼承於InputStream、OutputStream,字符流繼承於Reader、Writer。在java.io 包中還有許多其他的流,主要是爲了提高性能和使用方便。

補充:關於Java的IO需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不同於C#的是它只有一個維度一個方向。

補充:下面用IO和NIO兩種方式實現文件拷貝,這個題目在面試的時候是經常被問到的。

[java] view plaincopy
  1. package com.lovo;  
  2.   
  3. import java.io.FileInputStream;  
  4. import java.io.FileOutputStream;  
  5. import java.io.IOException;  
  6. import java.io.InputStream;  
  7. import java.io.OutputStream;  
  8. import java.nio.ByteBuffer;  
  9. import java.nio.channels.FileChannel;  
  10.   
  11. public class MyUtil {  
  12.   
  13.     private MyUtil() {  
  14.         throw new AssertionError();  
  15.     }  
  16.       
  17.     public static void fileCopy(String source, String target) throws IOException {  
  18.         try (InputStream in = new FileInputStream(source)) {  
  19.             try (OutputStream out = new FileOutputStream(target)) {  
  20.                 byte[] buffer = new byte[4096];  
  21.                 int bytesToRead;  
  22.                 while((bytesToRead = in.read(buffer)) != -1) {  
  23.                     out.write(buffer, 0, bytesToRead);  
  24.                 }  
  25.             }  
  26.         }  
  27.     }  
  28.       
  29.     public static void fileCopyNIO(String source, String target) throws IOException {  
  30.         try (FileInputStream in = new FileInputStream(source)) {  
  31.             try (FileOutputStream out = new FileOutputStream(target)) {  
  32.                 FileChannel inChannel = in.getChannel();  
  33.                 FileChannel outChannel = out.getChannel();  
  34.                 ByteBuffer buffer = ByteBuffer.allocate(4096);  
  35.                 while(inChannel.read(buffer) != -1) {  
  36.                     buffer.flip();  
  37.                     outChannel.write(buffer);  
  38.                     buffer.clear();  
  39.                 }  
  40.             }  
  41.         }  
  42.     }  
  43. }  

注意:上面用到Java 7的TWR,使用TWR後可以不用在finally中釋放外部資源 ,從而讓代碼更加優雅。


70、寫一個方法,輸入一個文件名和一個字符串,統計這個字符串在這個文件中出現的次數。

答:代碼如下:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.lovo.demo;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.FileReader;  
  5.   
  6. public class MyUtil {  
  7.   
  8.     // 工具類中的方法都是靜態方式訪問的因此將構造器私有不允許創建對象(絕對好習慣)  
  9.     private MyUtil() {  
  10.         throw new AssertionError();  
  11.     }  
  12.   
  13.     /** 
  14.      * 統計給定文件中給定字符串的出現次數 
  15.      *  
  16.      * @param filename  文件名 
  17.      * @param word 字符串 
  18.      * @return 字符串在文件中出現的次數 
  19.      */  
  20.     public static int countWordInFile(String filename, String word) {  
  21.         int counter = 0;  
  22.         try (FileReader fr = new FileReader(filename)) {  
  23.             try (BufferedReader br = new BufferedReader(fr)) {  
  24.                 String line = null;  
  25.                 while ((line = br.readLine()) != null) {  
  26.                     int index = -1;  
  27.                     while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {  
  28.                         counter++;  
  29.                         line = line.substring(index + word.length());  
  30.                     }  
  31.                 }  
  32.             }  
  33.         } catch (Exception ex) {  
  34.             ex.printStackTrace();  
  35.         }  
  36.         return counter;  
  37.     }  
  38.   
  39. }  

發佈了59 篇原創文章 · 獲贊 180 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章