近5年常考Java面試題及答案整理(二)

上一篇:近5年常考Java面試題及答案整理(一)

3一、String s = new String("xyz");建立了幾個字符串對象?html

答:兩個對象,一個是靜態區的"xyz",一個是用new建立在堆上的對象。java

3二、接口是否可繼承(extends)接口?抽象類是否可實現(implements)接口?抽象類是否可繼承具體類(concrete class)?程序員

答:接口能夠繼承接口,並且支持多重繼承。抽象類能夠實現(implements)接口,抽象類可繼承具體類也能夠繼承抽象類。面試

舉一個多繼承的例子,咱們定義一個動物(類)既是狗(父類1)也是貓(父類2),兩個父類都有「叫」這個方法。那麼當咱們調用「叫」這個方法時,它就不知道是狗叫仍是貓叫了,這就是多重繼承的衝突。
而接口沒有具體的方法實現,因此多繼承接口也不會出現這種衝突。算法

3三、一個".java"源文件中是否能夠包含多個類(不是內部類)?有什麼限制?數據庫

答:能夠,但一個源文件中最多隻能有一個公開類(public class)並且文件名必須和公開類的類名徹底保持一致。編程

3四、Anonymous Inner Class(匿名內部類)是否能夠繼承其它類?是否能夠實現接口?數組

答:能夠繼承其餘類或實現其餘接口,在Swing編程和Android開發中經常使用此方式來實現事件監聽和回調。瀏覽器

3五、內部類能夠引用它的包含類(外部類)的成員嗎?有沒有什麼限制?緩存

答:一個內部類對象能夠訪問建立它的外部類對象的成員,包括私有成員。

3六、Java 中的final關鍵字有哪些用法?

答:(1)修飾類:表示該類不能被繼承;(2)修飾方法:表示方法不能被重寫;(3)修飾變量:表示變量只能一次賦值之後值不能被修改(常量)。

3七、指出下面程序的運行結果。

 1 class A {
 2  
 3     static {
 4         System.out.print("1");
 5     }
 6  
 7     public A() {
 8         System.out.print("2");
 9     }
10 }
11  
12 class B extends A{
13  
14     static {
15         System.out.print("a");
16     }
17  
18     public B() {
19         System.out.print("b");
20     }
21 }
22  
23 public class Hello {
24  
25     public static void main(String[] args) {
26         A ab = new B();
27         ab = new B();
28     }
29  
30 }

答:執行結果:1a2b2b。建立對象時構造器的調用順序是:先初始化靜態成員,而後調用父類構造器,再初始化非靜態成員,最後調用自身構造器。

提示:若是不能給出此題的正確答案,說明以前第21題Java類加載機制尚未徹底理解,趕忙再看看吧。

3八、數據類型之間的轉換:
如何將字符串轉換爲基本數據類型?
如何將基本數據類型轉換爲字符串?

答:

  • 調用基本數據類型對應的包裝類中的方法parseXXX(String)或valueOf(String)便可返回相應基本類型;
  • 一種方法是將基本數據類型與空字符串("")鏈接(+)便可得到其所對應的字符串;另外一種方法是調用String 類中的valueOf()方法返回相應字符串

3九、如何實現字符串的反轉及替換?

答:方法不少,能夠本身寫實現也可使用String或StringBuffer/StringBuilder中的方法。有一道很常見的面試題是用遞歸實現字符串反轉,代碼以下所示:

1 public static String reverse(String originStr) {
2     if(originStr == null || originStr.length() <= 1) 
3         return originStr;
4     return reverse(originStr.substring(1)) + originStr.charAt(0);
5 }

40、怎樣將GB2312編碼的字符串轉換爲ISO-8859-1編碼的字符串?

答:代碼以下所示:

1 String s1 = "你好";
2 String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

4一、日期和時間:
如何取得年月日、小時分鐘秒?
如何取得從1970年1月1日0時0分0秒到如今的毫秒數?
如何取得某月的最後一天?
如何格式化日期?

答:
問題1:建立java.util.Calendar 實例,調用其get()方法傳入不一樣的參數便可得到參數所對應的值。Java 8中可使用java.time.LocalDateTimel來獲取,代碼以下所示。

 1 public class DateTimeTest {
 2     public static void main(String[] args) {
 3         Calendar cal = Calendar.getInstance();
 4         System.out.println(cal.get(Calendar.YEAR));
 5         System.out.println(cal.get(Calendar.MONTH));    // 0 - 11
 6         System.out.println(cal.get(Calendar.DATE));
 7         System.out.println(cal.get(Calendar.HOUR_OF_DAY));
 8         System.out.println(cal.get(Calendar.MINUTE));
 9         System.out.println(cal.get(Calendar.SECOND));
10  
11         // Java 8
12         LocalDateTime dt = LocalDateTime.now();
13         System.out.println(dt.getYear());
14         System.out.println(dt.getMonthValue());     // 1 - 12
15         System.out.println(dt.getDayOfMonth());
16         System.out.println(dt.getHour());
17         System.out.println(dt.getMinute());
18         System.out.println(dt.getSecond());
19     }
20 }

問題2:如下方法都可得到該毫秒數。

1 Calendar.getInstance().getTimeInMillis();
2 System.currentTimeMillis();
3 Clock.systemDefaultZone().millis(); // Java 8

問題3:代碼以下所示。

1 Calendar time = Calendar.getInstance();
2 time.getActualMaximum(Calendar.DAY_OF_MONTH);

問題4:利用java.text.DataFormat 的子類(如SimpleDateFormat類)中的format(Date)方法可將日期格式化。Java 8中能夠用java.time.format.DateTimeFormatter來格式化時間日期,代碼以下所示。

 1 import java.text.SimpleDateFormat;
 2 import java.time.LocalDate;
 3 import java.time.format.DateTimeFormatter;
 4 import java.util.Date;
 5  
 6 class DateFormatTest {
 7  
 8     public static void main(String[] args) {
 9         SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
10         Date date1 = new Date();
11         System.out.println(oldFormatter.format(date1));
12  
13         // Java 8
14         DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
15         LocalDate date2 = LocalDate.now();
16         System.out.println(date2.format(newFormatter));
17     }
18 }

補充:Java的時間日期API一直以來都是被詬病的東西,爲了解決這一問題,Java 8中引入了新的時間日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等類,這些的類的設計都使用了不變模式,所以是線程安全的設計。

4二、打印昨天的當前時刻。

1 import java.util.Calendar;
2  
3 class YesterdayCurrent {
4     public static void main(String[] args){
5         Calendar cal = Calendar.getInstance();
6         cal.add(Calendar.DATE, -1);
7         System.out.println(cal.getTime());
8     }
9 }

在Java 8中,能夠用下面的代碼實現相同的功能。

 1 import java.time.LocalDateTime;
 2  
 3 class YesterdayCurrent {
 4  
 5     public static void main(String[] args) {
 6         LocalDateTime today = LocalDateTime.now();
 7         LocalDateTime yesterday = today.minusDays(1);
 8  
 9         System.out.println(yesterday);
10     }
11 }

4三、比較一下Java和JavaSciprt。

答:JavaScript 與Java是兩個公司開發的不一樣的兩個產品。Java 是原Sun Microsystems公司推出的面向對象的程序設計語言,特別適合於互聯網應用程序開發;而JavaScript是Netscape公司的產品,爲了擴展Netscape瀏覽器的功能而開發的一種能夠嵌入Web頁面中運行的基於對象和事件驅動的解釋性語言。JavaScript的前身是LiveScript;而Java的前身是Oak語言。
下面對兩種語言間的異同做以下比較:

  • 基於對象和麪向對象:Java是一種真正的面向對象的語言,即便是開發簡單的程序,必須設計對象;JavaScript是種腳本語言,它能夠用來製做與網絡無關的,與用戶交互做用的複雜軟件。它是一種基於對象(Object-Based)和事件驅動(Event-Driven)的編程語言,於是它自己提供了很是豐富的內部對象供設計人員使用。
  • 解釋和編譯:Java的源代碼在執行以前,必須通過編譯。JavaScript是一種解釋性編程語言,其源代碼不需通過編譯,由瀏覽器解釋執行。(目前的瀏覽器幾乎都使用了JIT(即時編譯)技術來提高JavaScript的運行效率)
  • 強類型變量和弱類型變量:Java採用強類型變量檢查,即全部變量在編譯以前必須做聲明;JavaScript中變量是弱類型的,甚至在使用變量前能夠不做聲明,JavaScript的解釋器在運行時檢查推斷其數據類型。
  • 代碼格式不同。

補充:上面列出的四點是網上流傳的所謂的標準答案。其實Java和JavaScript最重要的區別是一個是靜態語言,一個是動態語言。目前的編程語言的發展趨勢是函數式語言和動態語言。在Java中類(class)是一等公民,而JavaScript中函數(function)是一等公民,所以JavaScript支持函數式編程,可使用Lambda函數和閉包(closure),固然Java 8也開始支持函數式編程,提供了對Lambda表達式以及函數式接口的支持。對於這類問題,在面試的時候最好仍是用本身的語言回答會更加靠譜,不要背網上所謂的標準答案。

4四、何時用斷言(assert)?

答:斷言在軟件開發中是一種經常使用的調試方式,不少開發語言中都支持這種機制。通常來講,斷言用於保證程序最基本、關鍵的正確性。斷言檢查一般在開發和測試時開啓。爲了保證程序的執行效率,在軟件發佈後斷言檢查一般是關閉的。斷言是一個包含布爾表達式的語句,在執行這個語句時假定該表達式爲true;若是表達式的值爲false,那麼系統會報告一個AssertionError。斷言的使用以下面的代碼所示:

1 assert(a > 0); // throws an AssertionError if a <= 0

斷言能夠有兩種形式:
assert Expression1;
assert Expression1 : Expression2 ;
Expression1 應該老是產生一個布爾值。
Expression2 能夠是得出一個值的任意表達式;這個值用於生成顯示更多調試信息的字符串消息。

要在運行時啓用斷言,能夠在啓動JVM時使用-enableassertions或者-ea標記。要在運行時選擇禁用斷言,能夠在啓動JVM時使用-da或者-disableassertions標記。要在系統類中啓用或禁用斷言,可以使用-esa或-dsa標記。還能夠在包的基礎上啓用或者禁用斷言。

注意:斷言不該該以任何方式改變程序的狀態。簡單的說,若是但願在不知足某些條件時阻止代碼的執行,就能夠考慮用斷言來阻止它。

4五、Error和Exception有什麼區別?

答:Error表示系統級的錯誤和程序沒必要處理的異常,是恢復不是不可能但很困難的狀況下的一種嚴重問題;好比內存溢出,不可能期望程序能處理這樣的狀況;Exception表示須要捕捉或者須要程序進行處理的異常,是一種設計或實現問題;也就是說,它表示若是程序運行正常,從不會發生的狀況。

面試題:2005年摩托羅拉的面試中曾經問過這麼一個問題「If a process reports a stack overflow run-time error, what’s the most possible cause?」,給了四個選項a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在運行時也可能會遭遇StackOverflowError,這是一個沒法恢復的錯誤,只能從新修改代碼了,這個面試題的答案是c。若是寫了不能迅速收斂的遞歸,則頗有可能引起棧溢出的錯誤,以下所示:

1 class StackOverflowErrorTest {
2  
3     public static void main(String[] args) {
4         main(null);
5     }
6 }

提示:用遞歸編寫程序時必定要牢記兩點:1. 遞歸公式;2. 收斂條件(何時就再也不繼續遞歸)。

4六、try{}裏有一個return語句,那麼緊跟在這個try後的finally{}裏的代碼會不會被執行,何時被執行,在return前仍是後?

答:會執行,在方法返回前執行。

注意:在finally中改變返回值的作法是很差的,由於若是存在finally代碼塊,try中的return語句不會立馬返回調用者,而是記錄下返回值待finally代碼塊執行完畢以後再向調用者返回其值,而後若是在finally中修改了返回值,就會返回修改後的值。顯然,在finally中返回或者修改返回值會對程序形成很大的困擾,C#中直接用編譯錯誤的方式來阻止程序員幹這種齷齪的事情,Java中也能夠經過提高編譯器的語法檢查級別來產生警告或錯誤,Eclipse中能夠在如圖所示的地方進行設置,強烈建議將此項設置爲編譯錯誤。

4七、Java語言如何進行異常處理,關鍵字:throws、throw、try、catch、finally分別如何使用?

答:Java經過面向對象的方法進行異常處理,把各類不一樣的異常進行分類,並提供了良好的接口。在Java中,每一個異常都是一個對象,它是Throwable類或其子類的實例。當一個方法出現異常後便拋出一個異常對象,該對象中包含有異常信息,調用這個對象的方法能夠捕獲到這個異常並能夠對其進行處理。Java的異常處理是經過5個關鍵詞來實現的:try、catch、throw、throws和finally。通常狀況下是用try來執行一段程序,若是系統會拋出(throw)一個異常對象,能夠經過它的類型來捕獲(catch)它,或經過老是執行代碼塊(finally)來處理;try用來指定一塊預防全部異常的程序;catch子句緊跟在try塊後面,用來指定你想要捕獲的異常的類型;throw語句用來明確地拋出一個異常;throws用來聲明一個方法可能拋出的各類異常(固然聲明異常時容許無病呻吟);finally爲確保一段代碼無論發生什麼異常情況都要被執行;try語句能夠嵌套,每當遇到一個try語句,異常的結構就會被放入異常棧中,直到全部的try語句都完成。若是下一級的try語句沒有對某種異常進行處理,異常棧就會執行出棧操做,直到遇到有處理這種異常的try語句或者最終將異常拋給JVM。

4八、運行時異常與受檢異常有何異同?

答:異常表示程序運行過程當中可能出現的非正常狀態,運行時異常表示虛擬機的一般操做中可能遇到的異常,是一種常見運行錯誤,只要程序設計得沒有問題一般就不會發生。受檢異常跟程序運行的上下文環境有關,即便程序設計無誤,仍然可能因使用的問題而引起。Java編譯器要求方法必須聲明拋出可能發生的受檢異常,可是並不要求必須聲明拋出未被捕獲的運行時異常。異常和繼承同樣,是面向對象程序設計中常常被濫用的東西,在Effective Java中對異常的使用給出瞭如下指導原則:

  • 不要將異常處理用於正常的控制流(設計良好的API不該該強迫它的調用者爲了正常的控制流而使用異常)
  • 對能夠恢復的狀況使用受檢異常,對編程錯誤使用運行時異常
  • 避免沒必要要的使用受檢異常(能夠經過一些狀態檢測手段來避免異常的發生)
  • 優先使用標準的異常
  • 每一個方法拋出的異常都要有文檔
  • 保持異常的原子性
  • 不要在catch中忽略掉捕獲到的異常

4九、列出一些你常見的運行時異常?

答:

  • ArithmeticException(算術異常)
  • ClassCastException (類轉換異常)
  • IllegalArgumentException (非法參數異常)
  • IndexOutOfBoundsException (下標越界異常)
  • NullPointerException (空指針異常)
  • SecurityException (安全異常)

50、闡述final、finally、finalize的區別。

答:

  • final:修飾符(關鍵字)有三種用法:若是一個類被聲明爲final,意味着它不能再派生出新的子類,即不能被繼承,所以它和abstract是反義詞。將變量聲明爲final,能夠保證它們在使用中不被改變,被聲明爲final的變量必須在聲明時給定初值,而在之後的引用中只能讀取不可修改。被聲明爲final的方法也一樣只能使用,不能在子類中被重寫。
  • finally:一般放在try…catch…的後面構造老是執行代碼塊,這就意味着程序不管正常執行仍是發生異常,這裏的代碼只要JVM不關閉都能執行,能夠將釋放外部資源的代碼寫在finally塊中。
  • finalize:Object類中定義的方法,Java中容許使用finalize()方法在垃圾收集器將對象從內存中清除出去以前作必要的清理工做。這個方法是由垃圾收集器在銷燬對象時調用的,經過重寫finalize()方法能夠整理系統資源或者執行其餘清理工做。

5一、類ExampleA繼承Exception,類ExampleB繼承ExampleA。
有以下代碼片段:

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編程思想》一書)

 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 }

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

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

5三、闡述ArrayList、Vector、LinkedList的存儲性能和特性。

答:ArrayList 和Vector都是使用數組方式存儲數據,此數組元素數大於實際存儲的數據以便增長和插入元素,它們都容許直接按序號索引元素,可是插入元素要涉及數組元素移動等內存操做,因此索引數據快而插入數據慢,Vector中的方法因爲添加了synchronized修飾,所以Vector是線程安全的容器,但性能上較ArrayList差,所以已是Java中的遺留容器。LinkedList使用雙向鏈表實現存儲(將內存中零散的內存單元經過附加的引用關聯起來,造成一個能夠按序號索引的線性結構,這種鏈式存儲方式與數組的連續存儲方式相比,內存的利用率更高),按序號索引數據須要進行前向或後向遍歷,可是插入數據時只須要記錄本項的先後項便可,因此插入速度較快。Vector屬於遺留容器(Java早期的版本中提供的容器,除此以外,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也是不正確的。Sun公司的工程師們也會犯這種低級錯誤,讓人唏噓不已。

5四、Collection和Collections的區別?

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

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

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

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

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

 1 public class Student implements Comparable<Student> {
 2     private String name;        // 姓名
 3     private int age;            // 年齡
 4  
 5     public Student(String name, int age) {
 6         this.name = name;
 7         this.age = age;
 8     }
 9  
10     @Override
11     public String toString() {
12         return "Student [name=" + name + ", age=" + age + "]";
13     }
14  
15     @Override
16     public int compareTo(Student o) {
17         return this.age - o.age; // 比較年齡(年齡的升序)
18     }
19  
20 }

 

 1 import java.util.Set;
 2 import java.util.TreeSet;
 3  
 4 class Test01 {
 5  
 6     public static void main(String[] args) {
 7         Set<Student> set = new TreeSet<>();     // Java 7的鑽石語法(構造器後面的尖括號中不須要寫類型)
 8         set.add(new Student("Hao LUO", 33));
 9         set.add(new Student("XJ WANG", 32));
10         set.add(new Student("Bruce LEE", 60));
11         set.add(new Student("Bob YANG", 22));
12  
13         for(Student stu : set) {
14             System.out.println(stu);
15         }
16 //      輸出結果: 
17 //      Student [name=Bob YANG, age=22]
18 //      Student [name=XJ WANG, age=32]
19 //      Student [name=Hao LUO, age=33]
20 //      Student [name=Bruce LEE, age=60]
21     }
22 }

例子2:

 1 public class Student {
 2     private String name;    // 姓名
 3     private int age;        // 年齡
 4  
 5     public Student(String name, int age) {
 6         this.name = name;
 7         this.age = age;
 8     }
 9  
10     /**
11      * 獲取學生姓名
12      */
13     public String getName() {
14         return name;
15     }
16  
17     /**
18      * 獲取學生年齡
19      */
20     public int getAge() {
21         return age;
22     }
23  
24     @Override
25     public String toString() {
26         return "Student [name=" + name + ", age=" + age + "]";
27     }
28  
29 }

 

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

5七、Thread類的sleep()方法和對象的wait()方法均可以讓線程暫停執行,它們有什麼區別?

答:sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其餘線程,可是對象的鎖依然保持,所以休眠時間結束後會自動恢復(線程回到就緒狀態,請參考第66題中的線程狀態轉換圖)。wait()是Object類的方法,調用對象的wait()方法致使當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),若是線程從新得到對象的鎖就能夠進入就緒狀態。

補充:可能很多人對什麼是進程,什麼是線程還比較模糊,對於爲何須要多線程編程也不是特別理解。簡單的說:進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,是操做系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是CPU調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時一般擁有獨立的內存單元,而線程之間能夠共享內存。使用多線程的編程一般可以帶來更好的性能和用戶體驗,可是多線程的程序對於其餘程序是不友好的,由於它可能佔用了更多的CPU資源。固然,也不是線程越多,程序的性能就越好,由於線程之間的調度和切換也會浪費CPU時間。時下很時髦的Node.js就採用了單線程異步I/O的工做模式。

5八、線程的sleep()方法和yield()方法有什麼區別?

答:
① sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
② 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
④ sleep()方法比yield()方法(跟操做系統CPU調度相關)具備更好的可移植性。

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

答:不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。由於非靜態方法上的synchronized修飾符要求執行方法時要得到對象的鎖,若是已經進入A方法說明對象鎖已經被取走,那麼試圖進入B方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。

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

答:

  • wait():使一個線程處於等待(阻塞)狀態,而且釋放所持有的對象的鎖;
  • sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理InterruptedException異常;
  • notify():喚醒一個處於等待狀態的線程,固然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM肯定喚醒哪一個線程,並且與優先級無關;
  • notityAll():喚醒全部處於等待狀態的線程,該方法並非將對象的鎖給全部線程,而是讓它們競爭,只有得到鎖的線程才能進入就緒狀態;

補充:Java 5經過Lock接口提供了顯式的鎖機制(explicit lock),加強了靈活性以及對線程的協調。Lock接口中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了newCondition()方法來產生用於線程之間通訊的Condition對象;此外,Java 5還提供了信號量機制(semaphore),信號量能夠用來限制對某個共享資源進行訪問的線程的數量。在對資源進行訪問以前,線程必須獲得信號量的許可(調用Semaphore對象的acquire()方法);在完成對資源的訪問後,線程必須向信號量歸還許可(調用Semaphore對象的release()方法)。

下面的例子演示了100個線程同時向一個銀行帳戶中存入1元錢,在沒有使用同步機制和使用同步機制狀況下的執行狀況。

銀行帳戶類:

 1 /**
 2  * 銀行帳戶
 3  * @author nnngu
 4  *
 5  */
 6 public class Account {
 7     private double balance;     // 帳戶餘額
 8  
 9     /**
10      * 存款
11      * @param money 存入金額
12      */
13     public void deposit(double money) {
14         double newBalance = balance + money;
15         try {
16             Thread.sleep(10);   // 模擬此業務須要一段處理時間
17         }
18         catch(InterruptedException ex) {
19             ex.printStackTrace();
20         }
21         balance = newBalance;
22     }
23  
24     /**
25      * 得到帳戶餘額
26      */
27     public double getBalance() {
28         return balance;
29     }
30 }

存錢線程類:

 1 /**
 2  * 存錢線程
 3  * @author nnngu
 4  *
 5  */
 6 public class AddMoneyThread implements Runnable {
 7     private Account account;    // 存入帳戶
 8     private double money;       // 存入金額
 9  
10     public AddMoneyThread(Account account, double money) {
11         this.account = account;
12         this.money = money;
13     }
14  
15     @Override
16     public void run() {
17         account.deposit(money);
18     }
19  
20 }

測試類:

 1 import java.util.concurrent.ExecutorService;
 2 import java.util.concurrent.Executors;
 3  
 4 public class Test01 {
 5  
 6     public static void main(String[] args) {
 7         Account account = new Account();
 8         ExecutorService service = Executors.newFixedThreadPool(100);
 9  
10         for(int i = 1; i <= 100; i++) {
11             service.execute(new AddMoneyThread(account, 1));
12         }
13  
14         service.shutdown();
15  
16         while(!service.isTerminated()) {}
17  
18         System.out.println("帳戶餘額: " + account.getBalance());
19     }
20 }

在沒有同步的狀況下,執行結果一般是顯示帳戶餘額在10元如下,出現這種情況的緣由是,當一個線程A試圖存入1元的時候,另一個線程B也可以進入存款的方法中,線程B讀取到的帳戶餘額仍然是線程A存入1元錢以前的帳戶餘額,所以也是在原來的餘額0上面作了加1元的操做,同理線程C也會作相似的事情,因此最後100個線程執行結束時,原本指望帳戶餘額爲100元,但實際獲得的一般在10元如下(極可能是1元哦)。解決這個問題的辦法就是同步,當一個線程對銀行帳戶存錢時,須要將此帳戶鎖定,待其操做完成後才容許其餘的線程進行操做,代碼有以下幾種調整方案:

在銀行帳戶的存款(deposit)方法上加同步(synchronized)關鍵字

 1 /**
 2  * 銀行帳戶
 3  * @author 張凱
 4  *
 5  */
 6 public class Account {
 7     private double balance;     // 帳戶餘額
 8  
 9     /**
10      * 存款
11      * @param money 存入金額
12      */
13     public synchronized void deposit(double money) {
14         double newBalance = balance + money;
15         try {
16             Thread.sleep(10);   // 模擬此業務須要一段處理時間
17         }
18         catch(InterruptedException ex) {
19             ex.printStackTrace();
20         }
21         balance = newBalance;
22     }
23  
24     /**
25      * 得到帳戶餘額
26      */
27     public double getBalance() {
28         return balance;
29     }
30 }

在線程調用存款方法時對銀行帳戶進行同步

 1 /**
 2  * 存錢線程
 3  * @author 張凱
 4  *
 5  */
 6 public class AddMoneyThread implements Runnable {
 7     private Account account;    // 存入帳戶
 8     private double money;       // 存入金額
 9  
10     public AddMoneyThread(Account account, double money) {
11         this.account = account;
12         this.money = money;
13     }
14  
15     @Override
16     public void run() {
17         synchronized (account) {
18             account.deposit(money); 
19         }
20     }
21  
22 }

經過Java 5顯示的鎖機制,爲每一個銀行帳戶建立一個鎖對象,在存款操做進行加鎖和解鎖的操做

 1 import java.util.concurrent.locks.Lock;
 2 import java.util.concurrent.locks.ReentrantLock;
 3  
 4 /**
 5  * 銀行帳戶
 6  * 
 7  * @author 張凱
 8  *
 9  */
10 public class Account {
11     private Lock accountLock = new ReentrantLock();
12     private double balance; // 帳戶餘額
13  
14     /**
15      * 存款
16      * 
17      * @param money
18      *            存入金額
19      */
20     public void deposit(double money) {
21         accountLock.lock();
22         try {
23             double newBalance = balance + money;
24             try {
25                 Thread.sleep(10); // 模擬此業務須要一段處理時間
26             }
27             catch (InterruptedException ex) {
28                 ex.printStackTrace();
29             }
30             balance = newBalance;
31         }
32         finally {
33             accountLock.unlock();
34         }
35     }
36  
37     /**
38      * 得到帳戶餘額
39      */
40     public double getBalance() {
41         return balance;
42     }
43 }

按照上述三種方式對代碼進行修改後,重寫執行測試代碼Test01,將看到最終的帳戶餘額爲100元。固然也可使用Semaphore或CountdownLatch來實現同步。

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

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

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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
 
class MyTask implements Callable<Integer> {
    private int upperBounds;
 
    public MyTask(int upperBounds) {
        this.upperBounds = upperBounds;
    }
 
    @Override
    public Integer call() throws Exception {
        int sum = 0; 
        for(int i = 1; i <= upperBounds; i++) {
            sum += i;
        }
        return sum;
    }
 
}
 
class Test {
 
    public static void main(String[] args) throws Exception {
        List<Future<Integer>> list = new ArrayList<>();
        ExecutorService service = Executors.newFixedThreadPool(10);
        for(int i = 0; i < 10; i++) {
            list.add(service.submit(new MyTask((int) (Math.random() * 100))));
        }
 
        int sum = 0;
        for(Future<Integer> future : list) {
            // while(!future.isDone()) ;
            sum += future.get();
        }
 
        System.out.println(sum);
    }
}

6二、synchronized關鍵字的用法?

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

6三、舉例說明同步和異步。

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

6四、啓動一個線程是調用run()仍是start()方法?

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

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

答:在面向對象編程中,建立和銷燬對象是很費時間的,由於建立一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每個對象,以便可以在對象銷燬後進行垃圾回收。因此提升服務程序效率的一個手段就是儘量減小建立和銷燬對象的次數,特別是一些很耗資源的對象建立和銷燬,這就是」池化資源」技術產生的緣由。線程池顧名思義就是事先建立若干個可執行的線程放入一個池(容器)中,須要的時候從池中獲取線程不用自行建立,使用完畢不須要銷燬線程而是放回池中,從而減小建立和銷燬線程對象的開銷。
Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,所以在工具類Executors裏面提供了一些靜態工廠方法,生成一些經常使用的線程池,以下所示:

  • newSingleThreadExecutor:建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool:建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。
  • newCachedThreadPool:建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小。
  • newScheduledThreadPool:建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

第60題的例子中演示了經過Executors工具類建立線程池並使用線程池執行線程的代碼。若是但願在服務器上使用線程池,強烈建議使用newFixedThreadPool方法來建立線程池,這樣能得到更好的性能。

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

答:

說明:其中Running表示運行狀態,Runnable表示就緒狀態(萬事俱備,只欠CPU),Blocked表示阻塞狀態,阻塞狀態又有多種狀況,多是由於調用wait()方法進入等待池,也多是執行同步方法或同步代碼塊進入等鎖池,或者是調用了sleep()方法或join()方法等待休眠或其餘線程結束,或是由於發生了I/O中斷。

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

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

相關文章
相關標籤/搜索