1. 程式人生 > >Java筆試面試題整理第五波

Java筆試面試題整理第五波

本系列整理Java相關的筆試面試知識點,其他幾篇文章如下:

1、實現多執行緒的兩種方法

實現多執行緒有兩種方法:繼承Thread和實現Runnable介面。繼承Thread:以賣票為例:
public class MyThread extends Thread {
    private static int COUNT = 5;
    private int ticket = COUNT;
    private String name;
    public MyThread(String s){
        name = s;
    }
    @Override
    public void run() {
        for(int i = 0; i < COUNT; i++){
            if(ticket > 0){
                System.out.println(name + "-->" + ticket--);
            }
        }
    }

測試使用:
MyThread thread1 = new MyThread("thread1");
        MyThread thread2 = new MyThread("thread2");
        thread1.start();
        thread2.start();
輸出:thread1-->5thread2-->5thread1-->4thread2-->4thread1-->3thread2-->3thread1-->2thread2-->2thread1-->1thread2-->1可以看到,這種方式每個執行緒自己擁有了一份票的數量,沒有實現票的數量共享。下面看實現Runnable的方式:
實現Runnable介面:
public class MyRunnable implements Runnable {
    private static int COUNT = 5;
    private int ticket = COUNT;

    @Override
    public void run() {
        for(int i = 0; i < COUNT; i++){
            if(ticket > 0){
                System.out.println("ticket-->" + ticket--);
            }
        }
    }
}
測試使用:
 MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
輸出:ticket-->5ticket-->3ticket-->2ticket-->1ticket-->4可以看到,實現Runnable的方式可以實現同一資源的共享實際工作中,一般使用實現Runnable介面的方式,是因為:(1)支援多個執行緒去處理同一資源,同時,執行緒程式碼和資料有效分離,體現了面向物件的思想;(2)避免了Java的單繼承性,如果使用繼承Thread的方式,那這個擴充套件類就不能再去繼承其他類。拓展:Thread的start()和run()方法區別:start()方法用於啟動一個執行緒,使其處於就緒狀態,得到了CPU就會執行,而直接呼叫run()方法,就相當於是普通的方法呼叫,會在主執行緒中直接執行,此時沒有開啟一個執行緒
可以看第一篇總結中,關於執行緒啟動下列方法中哪個是執行執行緒的方法? ()
A、run()    B、start()    C、sleep()    D、suspend()正確答案:Arun()方法用來執行執行緒體中具體的內容start()方法用來啟動執行緒物件,使其進入就緒狀態sleep()方法用來使執行緒進入睡眠狀態suspend()方法用來使執行緒掛起,要通過resume()方法使其重新啟動

2、訪問控制修飾符(新補充)

關於訪問控制修飾符,在第一篇總結中已有詳細的介紹。但最近在使用String類的一個方法compareTo()的時候,對private修飾符有了新的理解。String類的compareTo方法是用來比較兩個字串的字典序的,其原始碼如下:
public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;    //重點是這裡!!!
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;    //重點是這裡!!!

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }
    上面程式碼邏輯很好理解,我在看到它裡面直接使用anotherString.value來獲取String的字元陣列的時候很奇怪,因為value是被定義為private的,只能在類的內部使用,不能在外部通過類物件.變數名的方式訪問。我們平常都是通過String類的toCharArray()方法來獲取String的字元陣列的,看到上面的這種使用方法,我趕緊在別的地方測試了一下,發現的確是不能直接通過xx.value的方法來獲取字元陣列。這個問題我開始始終沒有想明白,特意發帖問了一下(http://www.imooc.com/wenda/detail/315660)。經過網友的提示,終於有點明白了。    正如前面所說的,value是被定義為private的,只能在類的內部使用,不能在外部通過類物件.變數名的方式訪問。因為compareTo方法就是String類的內部成員方法compareTo方法的引數傳遞的就是String物件過來,此時使用“類物件.變數名”的方式是在該類的內部使用,因此可以直接訪問到該類的私有成員。自己再模仿String類來測試一下,發現果然如此。。    問題很細節,但是沒有一下想通,說明還是對private的修飾符理解不夠到位,前面自認為只要是private修飾的,就不能通過“類物件.變數名”的方式訪問,其實還是需要看在哪裡面使用。

3、執行緒同步的方法

當我們有多個執行緒要訪問同一個變數或物件時,而這些執行緒中既有對改變數的讀也有寫操作時,就會導致變數值出現不可預知的情況。如下一個取錢和存錢的場景:沒有加入同步控制的情形:
public class BankCount {
    private int count = 0;//餘額

    public void addMoney(int money){//存錢
        count += money;
        System.out.println(System.currentTimeMillis() + "存入:" + money);
        System.out.println("賬戶餘額:" + count);
    }

    public void getMoney(int money){//取錢
        if(count - money < 0){
            System.out.println("餘額不足");
            System.out.println("賬戶餘額:" + count);
            return;
        }
        count -= money;
        System.out.println(System.currentTimeMillis() + "取出:" + money);
        System.out.println("賬戶餘額:" + count);
    }
}
測試類:
public class BankTest {
    public static void main(String[] args) {
        final BankCount bankCount = new BankCount();
        new Thread(new Runnable() {//取錢執行緒
            @Override
            public void run() {
                while(true){
                    bankCount.getMoney(200);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {//存錢執行緒
            @Override
            public void run() {
                while(true){
                    bankCount.addMoney(200);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}
部分列印結果如下:餘額不足賬戶餘額:01462265808958存入:200賬戶餘額:2001462265809959存入:200賬戶餘額:2001462265809959取出:200賬戶餘額:2001462265810959取出:200賬戶餘額:2001462265810959存入:200賬戶餘額:2001462265811959存入:200賬戶餘額:200可以看到,此時有兩個執行緒共同使用操作了bankCount物件中的count變數,使得count變數結果不符合預期。因此需要進行同步控制,同步控制的方法有以下幾種:(1)使用synchronized關鍵字同步方法每一個Java物件都有一個內建鎖,使用synchronized關鍵字修飾的方法,會使用Java的內建鎖作為鎖物件,來保護該方法。每個執行緒在呼叫該方法前,都需要獲得內建鎖,如果該鎖已被別的執行緒持有,當前執行緒就進入阻塞狀態修改BankCount 類中的兩個方法,如下:
public synchronized void addMoney(int money){//存錢
        count += money;
        System.out.println(System.currentTimeMillis() + "存入:" + money);
        System.out.println("賬戶餘額:" + count);
    }

    public synchronized void getMoney(int money){//取錢
        if(count - money < 0){
            System.out.println("餘額不足");
            System.out.println("賬戶餘額:" + count);
            return;
        }
        count -= money;
        System.out.println(System.currentTimeMillis() + "取出:" + money);
        System.out.println("賬戶餘額:" + count);
    }
執行測試列印如下結果:餘額不足賬戶餘額:01462266451171存入:200賬戶餘額:2001462266452171取出:200賬戶餘額:01462266452171存入:200賬戶餘額:2001462266453171存入:200賬戶餘額:4001462266453171取出:200賬戶餘額:2001462266454171存入:200賬戶餘額:4001462266454171取出:200賬戶餘額:2001462266455171取出:200賬戶餘額:0可以看到,列印結果符合我們的預期。另外,如果我們使用synchronized關鍵字來修飾static方法,此時呼叫該方法將會鎖住整個類。(關於類鎖、物件鎖下面有介紹)(2)使用synchronzied關鍵字同步程式碼塊使用synchronized關鍵字修飾的程式碼塊,會使用物件的內建鎖作為鎖物件,實現程式碼塊的同步。改造BankCount 類的兩個方法:
public void addMoney(int money){//存錢
        synchronized(this){
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("賬戶餘額:" + count);
        }
    }

    public void getMoney(int money){//取錢
        synchronized(this){
            if(count - money < 0){
                System.out.println("餘額不足");
                System.out.println("賬戶餘額:" + count);
                return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("賬戶餘額:" + count);
        }
    }
(注:這裡改造後的兩個方法中因為synchronized包含了方法體的整個程式碼語句,效率上與在方法名前加synchronized的第一種同步方法差不多,因為裡面涉及到了列印money還是需要同步的欄位,所以全部包含起來,僅僅是為了說明synchronized作用...)列印結果:餘額不足賬戶餘額:01462277436178存入:200賬戶餘額:2001462277437192存入:200賬戶餘額:4001462277437192取出:200賬戶餘額:2001462277438207取出:200賬戶餘額:01462277438207存入:200賬戶餘額:2001462277439222存入:200賬戶餘額:4001462277439222取出:200賬戶餘額:200可以看到,執行結果也符合我們的預期。synchronized同步方法和同步程式碼塊的選擇:同步是一種比較消耗效能的操作,應該儘量減少同步的內容,因此儘量使用同步程式碼塊的方式來進行同步操作,同步那些需要同步的語句(這些語句一般都訪問了一些共享變數)。但是像我們上面舉得這個例子,就不得不同步方法的整個程式碼塊,因為方法中的程式碼每條語句都涉及了共享變數,因此此時就可以直接使用synchronized同步方法的方式。(3)使用重入鎖(ReentrantLock)實現執行緒同步重入性:是指同一個執行緒多次試圖獲取它佔有的鎖,請求會成功,當釋放鎖的時候,直到重入次數為0,鎖才釋放完畢。ReentrantLock是介面Lock的一個具體實現類,和synchronized關鍵字具有相同的功能,並具有更高階的一些功能。如下使用:
public class BankCount {
    private Lock lock = new ReentrantLock();//獲取可重入鎖
    private int count = 0;//餘額

    public void addMoney(int money){//存錢
        lock.lock();
        try {
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("賬戶餘額:" + count);
        }finally{
            lock.unlock();
        }
    }

    public void getMoney(int money){//取錢
        lock.lock();
        try {
            if(count - money < 0){
                System.out.println("餘額不足");
                System.out.println("賬戶餘額:" + count);
                return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("賬戶餘額:" + count);
        } finally{
            lock.unlock();
        }
    }
}
部分列印結果:1462282419217存入:200賬戶餘額:2001462282420217取出:200賬戶餘額:01462282420217存入:200賬戶餘額:2001462282421217存入:200賬戶餘額:4001462282421217取出:200賬戶餘額:2001462282422217存入:200賬戶餘額:4001462282422217取出:200賬戶餘額:2001462282423217取出:200賬戶餘額:0同樣結果符合預期,說明使用ReentrantLock也是可以實現同步效果的。使用ReentrantLock時,lock()和unlock()需要成對出現,否則會出現死鎖,一般unlock都是放在finally中執行synchronized和ReentrantLock的區別和使用選擇:1、使用synchronized獲得的鎖存在一定缺陷>不能中斷一個正在試圖獲得鎖的執行緒>試圖獲得鎖時不能像ReentrantLock中的trylock那樣設定超時時間 ,當一個執行緒獲得了物件鎖後,其他執行緒訪問這個同步方法時,必須等待或阻塞,如果那個執行緒發生了死迴圈,物件鎖就永遠不會釋放;> 每個鎖只有單一的條件,不像condition那樣可以設定多個2、儘管synchronized存在上述的一些缺陷,在選擇上還是以synchronized優先>如果synchronized關鍵字適合程式,儘量使用它,可以減少程式碼出錯的機率程式碼數量 ;(減少出錯機率是因為在執行完synchronized包含完的最後一句語句後,鎖會自動釋放,不需要像ReentrantLock一樣手動寫unlock方法;)>如果特別需要Lock/Condition結構提供的獨有特性時,才使用他們 ;(比如設定一個執行緒長時間不能獲取鎖時設定超時時間或自我中斷等功能。)>許多情況下可以使用java.util.concurrent包中的一種機制,它會為你處理所有的加鎖情況;(比如當我們在多執行緒環境下使用HashMap時,可以使用ConcurrentHashMap來處理多執行緒併發)。下面兩種同步方式都是直接針對共享變數來設定的:(4)對共享變數使用volatile實現執行緒同步a.volatile關鍵字為變數的訪問提供了一種免鎖機制
b.使用volatile修飾域相當於告訴虛擬機器該域可能會被其他執行緒更新
c.因此每次使用該變數就要重新計算,直接從記憶體中獲取,而不是使用暫存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final型別的變數
修改BankCount類如下:
public class BankCount {
    private volatile int count = 0;//餘額

    public void addMoney(int money){//存錢
        count += money;
        System.out.println(System.currentTimeMillis() + "存入:" + money);
        System.out.println("賬戶餘額:" + count);
    }

    public void getMoney(int money){//取錢
        if(count - money < 0){
            System.out.println("餘額不足");
            System.out.println("賬戶餘額:" + count);
            return;
        }
        count -= money;
        System.out.println(System.currentTimeMillis() + "取出:" + money);
        System.out.println("賬戶餘額:" + count);
    }
}
部分列印結果:餘額不足賬戶餘額:2001462286786371存入:200賬戶餘額:2001462286787371存入:200賬戶餘額:2001462286787371取出:200賬戶餘額:2001462286788371取出:2001462286788371存入:200賬戶餘額:200賬戶餘額:2001462286789371存入:200賬戶餘額:200可以看到,使用volitale修飾變數,並不能保證執行緒的同步。volitale相當於一種“輕量級的synchronized”,但是它不能代替synchronized,volitale的使用有較強的限制,它要求該變數狀態真正獨立於程式內其他內容時才能使用 volatile。volitle的原理是每次執行緒要訪問volatile修飾的變數時都是從記憶體中讀取,而不是從快取當中讀取,以此來保證同步(這種原理方式正如上面例子看到的一樣,多執行緒的條件下很多情況下還是會存在很大問題的)。因此,我們儘量不會去使用volitale。(5)ThreadLocal實現同步區域性變數使用ThreadLocal管理變數,則每一個使用該變數的執行緒都獲得該變數的副本副本之間相互獨立,這樣每一個執行緒都可以隨意修改自己的變數副本,而不會對其他執行緒產生影響
ThreadLocal的主要方法有:1、initialValue():返回當前執行緒賦予當前執行緒拷貝的區域性執行緒變數的初始值。一般在定義ThreadLocal類的時候會重寫該方法,返回初始值;2、get():返回當前執行緒拷貝的區域性執行緒變數的值;3、set(T value):為當前執行緒拷貝的區域性執行緒變數設定一個特定的值;4、remove():移除當前執行緒賦予區域性執行緒變數的值如下使用:
public class BankCount {
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        protected Integer initialValue() {
            return 0;
        };
    };//餘額

    public void addMoney(int money){//存錢
        count.set(count.get() + money);
        System.out.println(System.currentTimeMillis() + "存入:" + money);
        System.out.println("賬戶餘額:" + count.get());
    }

    public void getMoney(int money){//取錢
        if(count.get() - money < 0){
            System.out.println("餘額不足");
            System.out.println("賬戶餘額:" + count.get());
            return;
        }
        count.set(count.get() - money);
        System.out.println(System.currentTimeMillis() + "取出:" + money);
        System.out.println("賬戶餘額:" + count.get());
    }
}
部分列印結果:餘額不足1462289139008存入:200賬戶餘額:0賬戶餘額:200餘額不足賬戶餘額:01462289140008存入:200賬戶餘額:400餘額不足賬戶餘額:01462289141008存入:200賬戶餘額:600餘額不足賬戶餘額:0從列印結果可以看到,測試類中的兩個執行緒分別擁有了一份count拷貝,即取錢執行緒和存錢執行緒都有一個count初始值為0的變數,因此可以一直存錢但是不能取錢ThreadLocal使用時機:由於ThreadLocal管理的區域性變數對於每個執行緒都會產生一份單獨的拷貝,因此ThreadLocal適合用來管理與執行緒相關的關聯狀態,典型的管理區域性變數是private static型別的,比如使用者ID、事物ID,我們的伺服器應用框架對於每一個請求都是用一個單獨的執行緒中處理,所以事物ID對每一個執行緒是唯一的,此時用ThreadLocal來管理這個事物ID,就可以從每個執行緒中獲取事物ID了。ThreadLocal和前面幾種同步機制的比較:1、hreadLocal和其它所有的同步機制都是為了解決多執行緒中的對同一變數的訪問衝突,在普通的同步機制中,是通過物件加鎖來實現多個執行緒對同一變數的安全訪問的。這時該變數是多個執行緒共享的,使用這種同步機制需要很細緻地分析在什麼時候對變數進行讀寫,什麼時候需要鎖定某個物件,什麼時候釋放該物件的鎖等等很多。所有這些都是因為多個執行緒共享了資源造成的。2、ThreadLocal就從另一個角度來解決多執行緒的併發訪問,ThreadLocal會為每一個執行緒維護一個和該執行緒繫結的變數的副本,從而隔離了多個執行緒的資料,每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,在編寫多執行緒程式碼時,可以把不安全的整個變數封裝進ThreadLocal,或者把該物件的特定於執行緒的狀態封裝進ThreadLocal3、ThreadLocal並不能替代同步機制,兩者面向的問題領域不同。同步機制是為了同步多個執行緒對相同資源的併發訪問,是為了多個執行緒之間進行通訊的有效方式;而ThreadLocal是隔離多個執行緒的資料共享,從根本上就不在多個執行緒之間共享資源(變數),這樣當然不需要對多個執行緒進行同步了。所以,如果你需要進行多個執行緒之間進行通訊,則使用同步機制;如果需要隔離多個執行緒之間的共享衝突,可以使用ThreadLocal,這將極大地簡化你的程式,使程式更加易讀、簡潔。 

4、鎖的等級:方法鎖、物件鎖、類鎖

    Java中每個物件例項都可以作為一個實現同步的鎖,也即物件鎖(或內建鎖),當使用synchronized修飾普通方法時,也叫方法鎖(對於方法鎖這個概念我覺得只是一種叫法,因為此時用來鎖住方法的可能是物件鎖也可能是類鎖),當我們用synchronized修飾static方法時,此時的鎖是類鎖    物件鎖的實現方法    1、用synchronized修飾普通方法(非static)    2、用synchronized(this){...}的形式包括程式碼塊上面兩種方式獲得的鎖是同一個鎖物件,即當前的例項物件鎖。(當然,也可以使用其他傳過來的例項物件作為鎖物件),如下例項:
public class BankCount {
    public synchronized void addMoney(int money){//存錢
       synchronized(this){    //同步程式碼塊
            int i = 5;
            while(i-- > 0){
                System.out.println(Thread.currentThread().getName() + ">存入:" + money);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public synchronized void getMoney(int money){//取錢
        int i = 5;
        while(i-- > 0){
            System.out.println(Thread.currentThread().getName() + ">取錢:" + money);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
測試類:
public class BankTest {
    public static void main(String[] args) {
        final BankCount bankCount = new BankCount();
        new Thread(new Runnable() {//取錢執行緒
            @Override
            public void run() {
                bankCount.getMoney(200);
            }
        },"取錢執行緒").start();

        new Thread(new Runnable() {//存錢執行緒
            @Override
            public void run() {
                bankCount.addMoney(200);
            }
        },"存錢執行緒").start();
    }
}
列印結果如下:取錢執行緒>取錢:200取錢執行緒>取錢:200取錢執行緒>取錢:200取錢執行緒>取錢:200取錢執行緒>取錢:200存錢執行緒>存入:200存錢執行緒>存入:200存錢執行緒>存入:200存錢執行緒>存入:200存錢執行緒>存入:200列印結果表明,synchronized修飾的普通方法和程式碼塊獲得的是同一把鎖,才會使得一個執行緒執行一個執行緒等待的執行結果。   類鎖的實現方法:    1、使用synchronized修飾static方法    2、使用synchronized(類名.class){...}的形式包含程式碼塊因為static的方法是屬於類的,因此synchronized修飾的static方法獲取到的肯定是類鎖,一個類可以有很多物件,但是這個類只會有一個.class的二進位制檔案,因此這兩種方式獲得的也是同一種類鎖如下修改一下上面程式碼的兩個方法:
public void addMoney(int money){//存錢
        synchronized(BankCount.class){
            int i = 5;
            while(i-- > 0){
                System.out.println(Thread.currentThread().getName() + ">存入:" + money);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static synchronized void getMoney(int money){//取錢
        int i = 5;
        while(i-- > 0){
            System.out.println(Thread.currentThread().getName() + ">取錢:" + money);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
列印結果和上面一樣。說明這兩種方式獲得的鎖是同一種類鎖。類鎖和物件鎖是兩種不同的鎖物件,如果將addMoney方法改為普通的物件鎖方式,繼續測試,可以看到列印結果是交替進行的。注:(1)一個執行緒獲得了物件鎖或者類鎖,其他執行緒還是可以訪問其他非同步方法,獲得了鎖只是阻止了其他執行緒訪問使用相同鎖的方法、程式碼塊;       (2)一個獲得了物件鎖的執行緒,可以在該同步方法中繼續去訪問其他相同鎖物件的同步方法,而不需要重新申請鎖。後面一篇,將總結執行緒池ThreadPool、生產者消費者問題及實現、sleep和wait方法區別。【參考文章: