1. 程式人生 > >近5年常考Java面試題及答案整理(二)

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

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

下列面試題都是在網上收集的,本人抱著學習的態度找了下參考答案,有不足的地方還請指正,更多精彩內容可以關注我的微信公眾號:Java團長

31、String s = new String("xyz");建立了幾個字串物件?

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

32、介面是否可繼承(extends)介面?抽象類是否可實現(implements)介面?抽象類是否可繼承具體類(concrete class)?

答:介面可以繼承介面,而且支援多重繼承。抽象類可以實現(implements)介面,抽象類可繼承具體類也可以繼承抽象類。

舉一個多繼承的例子,我們定義一個動物(類)既是狗(父類1)也是貓(父類2),兩個父類都有“叫”這個方法。那麼當我們呼叫“叫”這個方法時,它就不知道是狗叫還是貓叫了,這就是多重繼承的衝突。
而介面沒有具體的方法實現,所以多繼承介面也不會出現這種衝突。

33、一個".java"原始檔中是否可以包含多個類(不是內部類)?有什麼限制?

答:可以,但一個原始檔中最多隻能有一個公開類(public class)而且檔名必須和公開類的類名完全保持一致。

34、Anonymous Inner Class(匿名內部類)是否可以繼承其它類?是否可以實現介面?

答:可以繼承其他類或實現其他介面,在Swing程式設計和Android開發中常用此方式來實現事件監聽和回撥。

35、內部類可以引用它的包含類(外部類)的成員嗎?有沒有什麼限制?

答:一個內部類物件可以訪問建立它的外部類物件的成員,包括私有成員。

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

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

37、指出下面程式的執行結果。

class A {

    static {
        System.out.print("1");
    }

    public A() {
        System.out.print("2");
    }
}

class B extends A{

    static {
        System.out.print("a");
    }

    public B() {
        System.out.print("b");
    }
}

public class Hello {

    public static void main(String[] args) {
        A ab = new B();
        ab = new B();
    }

}

答:執行結果:1a2b2b。建立物件時構造器的呼叫順序是:先初始化靜態成員,然後呼叫父類構造器,再初始化非靜態成員,最後呼叫自身構造器。

提示:如果不能給出此題的正確答案,說明之前第21題Java類載入機制還沒有完全理解,趕緊再看看吧。

38、資料型別之間的轉換:
如何將字串轉換為基本資料型別?
如何將基本資料型別轉換為字串?

答:

  • 呼叫基本資料型別對應的包裝類中的方法parseXXX(String)或valueOf(String)即可返回相應基本型別;
  • 一種方法是將基本資料型別與空字串("")連線(+)即可獲得其所對應的字串;另一種方法是呼叫String 類中的valueOf()方法返回相應字串

39、如何實現字串的反轉及替換?

答:方法很多,可以自己寫實現也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常見的面試題是用遞迴實現字串反轉,程式碼如下所示:

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

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

答:程式碼如下所示:

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

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

答:
問題1:建立java.util.Calendar 例項,呼叫其get()方法傳入不同的引數即可獲得引數所對應的值。Java 8中可以使用java.time.LocalDateTimel來獲取,程式碼如下所示。

public class DateTimeTest {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        System.out.println(cal.get(Calendar.YEAR));
        System.out.println(cal.get(Calendar.MONTH));    // 0 - 11
        System.out.println(cal.get(Calendar.DATE));
        System.out.println(cal.get(Calendar.HOUR_OF_DAY));
        System.out.println(cal.get(Calendar.MINUTE));
        System.out.println(cal.get(Calendar.SECOND));

        // Java 8
        LocalDateTime dt = LocalDateTime.now();
        System.out.println(dt.getYear());
        System.out.println(dt.getMonthValue());     // 1 - 12
        System.out.println(dt.getDayOfMonth());
        System.out.println(dt.getHour());
        System.out.println(dt.getMinute());
        System.out.println(dt.getSecond());
    }
}

問題2:以下方法均可獲得該毫秒數。

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

問題3:程式碼如下所示。

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

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

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;

class DateFormatTest {

    public static void main(String[] args) {
        SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
        Date date1 = new Date();
        System.out.println(oldFormatter.format(date1));

        // Java 8
        DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        LocalDate date2 = LocalDate.now();
        System.out.println(date2.format(newFormatter));
    }
}

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

42、列印昨天的當前時刻。

import java.util.Calendar;

class YesterdayCurrent {
    public static void main(String[] args){
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -1);
        System.out.println(cal.getTime());
    }
}

在Java 8中,可以用下面的程式碼實現相同的功能。

import java.time.LocalDateTime;

class YesterdayCurrent {

    public static void main(String[] args) {
        LocalDateTime today = LocalDateTime.now();
        LocalDateTime yesterday = today.minusDays(1);

        System.out.println(yesterday);
    }
}

43、比較一下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表示式以及函式式介面的支援。對於這類問題,在面試的時候最好還是用自己的語言回答會更加靠譜,不要背網上所謂的標準答案。

44、什麼時候用斷言(assert)?

答:斷言在軟體開發中是一種常用的除錯方式,很多開發語言中都支援這種機制。一般來說,斷言用於保證程式最基本、關鍵的正確性。斷言檢查通常在開發和測試時開啟。為了保證程式的執行效率,在軟體釋出後斷言檢查通常是關閉的。斷言是一個包含布林表示式的語句,在執行這個語句時假定該表示式為true;如果表示式的值為false,那麼系統會報告一個AssertionError。斷言的使用如下面的程式碼所示:

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

斷言可以有兩種形式:
assert Expression1;
assert Expression1 : Expression2 ;
Expression1 應該總是產生一個布林值。
Expression2 可以是得出一個值的任意表達式;這個值用於生成顯示更多除錯資訊的字串訊息。

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

注意:斷言不應該以任何方式改變程式的狀態。簡單的說,如果希望在不滿足某些條件時阻止程式碼的執行,就可以考慮用斷言來阻止它。

45、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。如果寫了不能迅速收斂的遞迴,則很有可能引發棧溢位的錯誤,如下所示:

class StackOverflowErrorTest {

    public static void main(String[] args) {
        main(null);
    }
}

提示:用遞迴編寫程式時一定要牢記兩點:1. 遞迴公式;2. 收斂條件(什麼時候就不再繼續遞迴)。

46、try{}裡有一個return語句,那麼緊跟在這個try後的finally{}裡的程式碼會不會被執行,什麼時候被執行,在return前還是後?

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

注意:在finally中改變返回值的做法是不好的,因為如果存在finally程式碼塊,try中的return語句不會立馬返回呼叫者,而是記錄下返回值待finally程式碼塊執行完畢之後再向呼叫者返回其值,然後如果在finally中修改了返回值,就會返回修改後的值。顯然,在finally中返回或者修改返回值會對程式造成很大的困擾,C#中直接用編譯錯誤的方式來阻止程式設計師幹這種齷齪的事情,Java中也可以通過提升編譯器的語法檢查級別來產生警告或錯誤,Eclipse中可以在如圖所示的地方進行設定,強烈建議將此項設定為編譯錯誤。

47、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。

48、執行時異常與受檢異常有何異同?

答:異常表示程式執行過程中可能出現的非正常狀態,執行時異常表示虛擬機器的通常操作中可能遇到的異常,是一種常見執行錯誤,只要程式設計得沒有問題通常就不會發生。受檢異常跟程式執行的上下文環境有關,即使程式設計無誤,仍然可能因使用的問題而引發。Java編譯器要求方法必須宣告丟擲可能發生的受檢異常,但是並不要求必須宣告丟擲未被捕獲的執行時異常。異常和繼承一樣,是面向物件程式設計中經常被濫用的東西,在Effective Java中對異常的使用給出了以下指導原則:

  • 不要將異常處理用於正常的控制流(設計良好的API不應該強迫它的呼叫者為了正常的控制流而使用異常)
  • 對可以恢復的情況使用受檢異常,對程式設計錯誤使用執行時異常
  • 避免不必要的使用受檢異常(可以通過一些狀態檢測手段來避免異常的發生)
  • 優先使用標準的異常
  • 每個方法丟擲的異常都要有文件
  • 保持異常的原子性
  • 不要在catch中忽略掉捕獲到的異常

49、列出一些你常見的執行時異常?

答:

  • 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()方法可以整理系統資源或者執行其他清理工作。

51、類ExampleA繼承Exception,類ExampleB繼承ExampleA。
有如下程式碼片斷:

try {
    throw new ExampleB("b")
} catch(ExampleA e){
    System.out.println("ExampleA");
} catch(Exception e){
    System.out.println("Exception");
}

請問執行此段程式碼的輸出是什麼?

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

面試題 - 說出下面程式碼的執行結果。(此題的出處是《Java程式設計思想》一書)

class Annoyance extends Exception {}
class Sneeze extends Annoyance {}

class Human {

    public static void main(String[] args) 
        throws Exception {
        try {
            try {
                throw new Sneeze();
            } 
            catch ( Annoyance a ) {
                System.out.println("Caught Annoyance");
                throw a;
            }
        } 
        catch ( Sneeze s ) {
            System.out.println("Caught Sneeze");
            return ;
        }
        finally {
            System.out.println("Hello World!");
        }
    }
}

52、List、Set、Map是否繼承自Collection介面?

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

53、闡述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公司的工程師們也會犯這種低階錯誤,讓人唏噓不已。

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方法實現元素的比較),相當於一個臨時定義的排序規則,其實就是通過介面注入比較元素大小的演算法,也是對回撥模式的應用(Java中對函數語言程式設計的支援)。
例子1:

public class Student implements Comparable<Student> {
    private String name;        // 姓名
    private int age;            // 年齡

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age; // 比較年齡(年齡的升序)
    }

}
import java.util.Set;
import java.util.TreeSet;

class Test01 {

    public static void main(String[] args) {
        Set<Student> set = new TreeSet<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫型別)
        set.add(new Student("Hao LUO", 33));
        set.add(new Student("XJ WANG", 32));
        set.add(new Student("Bruce LEE", 60));
        set.add(new Student("Bob YANG", 22));

        for(Student stu : set) {
            System.out.println(stu);
        }
//      輸出結果: 
//      Student [name=Bob YANG, age=22]
//      Student [name=XJ WANG, age=32]
//      Student [name=Hao LUO, age=33]
//      Student [name=Bruce LEE, age=60]
    }
}

例子2:

public class Student {
    private String name;    // 姓名
    private int age;        // 年齡

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 獲取學生姓名
     */
    public String getName() {
        return name;
    }

    /**
     * 獲取學生年齡
     */
    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Test02 {

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();     // Java 7的鑽石語法(構造器後面的尖括號中不需要寫型別)
        list.add(new Student("Hao LUO", 33));
        list.add(new Student("XJ WANG", 32));
        list.add(new Student("Bruce LEE", 60));
        list.add(new Student("Bob YANG", 22));

        // 通過sort方法的第二個引數傳入一個Comparator介面物件
        // 相當於是傳入一個比較物件大小的演算法到sort方法中
        // 由於Java中沒有函式指標、仿函式、委託這樣的概念
        // 因此要將一個演算法傳入一個方法中唯一的選擇就是通過介面回撥
        Collections.sort(list, new Comparator<Student> () {

            @Override
            public int compare(Student o1, Student o2) {
                return o1.getName().compareTo(o2.getName());    // 比較學生姓名
            }
        });

        for(Student stu : list) {
            System.out.println(stu);
        }
//      輸出結果: 
//      Student [name=Bob YANG, age=22]
//      Student [name=Bruce LEE, age=60]
//      Student [name=Hao LUO, age=33]
//      Student [name=XJ WANG, age=32]
    }
}

57、Thread類的sleep()方法和物件的wait()方法都可以讓執行緒暫停執行,它們有什麼區別?

答:sleep()方法(休眠)是執行緒類(Thread)的靜態方法,呼叫此方法會讓當前執行緒暫停執行指定的時間,將執行機會(CPU)讓給其他執行緒,但是物件的鎖依然保持,因此休眠時間結束後會自動恢復(執行緒回到就緒狀態,請參考第66題中的執行緒狀態轉換圖)。wait()是Object類的方法,呼叫物件的wait()方法導致當前執行緒放棄物件的鎖(執行緒暫停執行),進入物件的等待池(wait pool),只有呼叫物件的notify()方法(或notifyAll()方法)時才能喚醒等待池中的執行緒進入等鎖池(lock pool),如果執行緒重新獲得物件的鎖就可以進入就緒狀態。

補充:可能不少人對什麼是程序,什麼是執行緒還比較模糊,對於為什麼需要多執行緒程式設計也不是特別理解。簡單的說:程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,是作業系統進行資源分配和排程的一個獨立單位;執行緒是程序的一個實體,是CPU排程和分派的基本單位,是比程序更小的能獨立執行的基本單位。執行緒的劃分尺度小於程序,這使得多執行緒程式的併發性高;程序在執行時通常擁有獨立的記憶體單元,而執行緒之間可以共享記憶體。使用多執行緒的程式設計通常能夠帶來更好的效能和使用者體驗,但是多執行緒的程式對於其他程式是不友好的,因為它可能佔用了更多的CPU資源。當然,也不是執行緒越多,程式的效能就越好,因為執行緒之間的排程和切換也會浪費CPU時間。時下很時髦的Node.js就採用了單執行緒非同步I/O的工作模式。

58、執行緒的sleep()方法和yield()方法有什麼區別?

答:
① sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
② 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常;
④ sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。

59、當一個執行緒進入一個物件的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元錢,在沒有使用同步機制和使用同步機制情況下的執行情況。

銀行賬戶類:

/**
 * 銀行賬戶
 * @author nnngu
 *
 */
public class Account {
    private double balance;     // 賬戶餘額

    /**
     * 存款
     * @param money 存入金額
     */
    public void deposit(double money) {
        double newBalance = balance + money;
        try {
            Thread.sleep(10);   // 模擬此業務需要一段處理時間
        }
        catch(InterruptedException ex) {
            ex.printStackTrace();
        }
        balance = newBalance;
    }

    /**
     * 獲得賬戶餘額
     */
    public double getBalance() {
        return balance;
    }
}

存錢執行緒類:

/**
 * 存錢執行緒
 * @author nnngu
 *
 */
public class AddMoneyThread implements Runnable {
    private Account account;    // 存入賬戶
    private double money;       // 存入金額

    public AddMoneyThread(Account account, double money) {
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        account.deposit(money);
    }

}

測試類:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test01 {

    public static void main(String[] args) {
        Account account = new Account();
        ExecutorService service = Executors.newFixedThreadPool(100);

        for(int i = 1; i <= 100; i++) {
            service.execute(new AddMoneyThread(account, 1));
        }

        service.shutdown();

        while(!service.isTerminated()) {}

        System.out.println("賬戶餘額: " + account.getBalance());
    }
}

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

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

/**
 * 銀行賬戶
 * @author 張凱
 *
 */
public class Account {
    private double balance;     // 賬戶餘額

    /**
     * 存款
     * @param money 存入金額
     */
    public synchronized void deposit(double money) {
        double newBalance = balance + money;
        try {
            Thread.sleep(10);   // 模擬此業務需要一段處理時間
        }
        catch(InterruptedException ex) {
            ex.printStackTrace();
        }
        balance = newBalance;
    }

    /**
     * 獲得賬戶餘額
     */
    public double getBalance() {
        return balance;
    }
}

線上程呼叫存款方法時對銀行賬戶進行同步

/**
 * 存錢執行緒
 * @author 張凱
 *
 */
public class AddMoneyThread implements Runnable {
    private Account account;    // 存入賬戶
    private double money;       // 存入金額

    public AddMoneyThread(Account account, double money) {
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        synchronized (account) {
            account.deposit(money); 
        }
    }

}

通過Java 5顯示的鎖機制,為每個銀行賬戶建立一個鎖物件,在存款操作進行加鎖和解鎖的操作

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 銀行賬戶
 * 
 * @author 張凱
 *
 */
public class Account {
    private Lock accountLock = new ReentrantLock();
    private double balance; // 賬戶餘額

    /**
     * 存款
     * 
     * @param money
     *            存入金額
     */
    public void deposit(double money) {
        accountLock.lock();
        try {
            double newBalance = balance + money;
            try {
                Thread.sleep(10); // 模擬此業務需要一段處理時間
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            balance = newBalance;
        }
        finally {
            accountLock.unlock();
        }
    }

    /**
     * 獲得賬戶餘額
     */
    public double getBalance() {
        return balance;
    }
}

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

61、編寫多執行緒程式有幾種實現方式?

答: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);
    }
}

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:建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

第60題的例子中演示了通過Executors工具類建立執行緒池並使用執行緒池執行執行緒的程式碼。如果希望在伺服器上使用執行緒池,強烈建議使用newFixedThreadPool方法來建立執行緒池,這樣能獲得更好的效能。

66、執行緒的基本狀態以及狀態之間的關係?

答:

說明:其中Running表示執行狀態,Runnable表示就緒狀態(萬事俱備,只欠CPU),Blocked表示阻塞狀態,阻塞狀態又有多種情況,可能是因為呼叫wait()方法進入等待池,也可能是執行同步方法或同步程式碼塊進入等鎖池,或者是呼叫了sleep()方法或join()方法等待休眠或其他執行緒結束,或是因為發生了I/O中斷。

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

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

我有一個微信公眾號,經常會分享一些Java技術相關的乾貨;如果你喜歡我的分享,可以用微信搜尋“Java團長”或者“javatuanzhang”關注。

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