1. 程式人生 > >2018中級java面試題

2018中級java面試題

建立執行緒的方式及實現

Java中建立執行緒主要有三種方式:

一、繼承Thread類建立執行緒類

(1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。

(2)建立Thread子類的例項,即建立了執行緒物件。

(3)呼叫執行緒物件的start()方法來啟動該執行緒。

package com.thread;   public class FirstThreadTest extends Thread{     int i = 0;     //重寫run方法,run方法的方法體就是現場執行體     public void run()     {         for(;i<100;i++){           System.out.println(getName()+"  "+i);         }     }     public static void main(String[] args)     {         for(int i = 0;i< 100;i++)         {             System.out.println(Thread.currentThread().getName()+"  : "+i);             if(i==20)             {                 new FirstThreadTest().start();                 new FirstThreadTest().start();             }         }     } } 上述程式碼中Thread.currentThread()方法返回當前正在執行的執行緒物件。getName()方法返回呼叫該方法的執行緒的名字。

二、通過Runnable介面建立執行緒類

(1)定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。

(2)建立 Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。

(3)呼叫執行緒物件的start()方法來啟動該執行緒。

package com.wityx;   public class RunnableThreadTest implements Runnable {       private int i;     public void run()     {         for(i = 0;i <100;i++)         {             System.out.println(Thread.currentThread().getName()+" "+i);         }     }     public static void main(String[] args)     {         for(int i = 0;i < 100;i++)         {             System.out.println(Thread.currentThread().getName()+" "+i);             if(i==20)             {                 RunnableThreadTest rtt = new RunnableThreadTest();                 new Thread(rtt,"新執行緒1").start();                 new Thread(rtt,"新執行緒2").start();             }         }     } } 三、通過Callable和Future建立執行緒

(1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。

(2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。

(3)使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。

(4)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

package com.thread;   import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;   public class CallableThreadTest implements Callable<Integer> {     public static void main(String[] args)     {         CallableThreadTest ctt = new CallableThreadTest();         FutureTask<Integer> ft = new FutureTask<>(ctt);         for(int i = 0;i < 100;i++)         {             System.out.println(Thread.currentThread().getName()+" 的迴圈變數i的值"+i);             if(i==20)             {                 new Thread(ft,"有返回值的執行緒").start();             }         }         try         {             System.out.println("子執行緒的返回值:"+ft.get());         } catch (InterruptedException e)         {             e.printStackTrace();         } catch (ExecutionException e)         {             e.printStackTrace();         }     }     @Override     public Integer call() throws Exception     {         int i = 0;         for(;i<100;i++)         {             System.out.println(Thread.currentThread().getName()+" "+i);         }         return i;     } } 建立執行緒的三種方式的對比

採用實現Runnable、Callable介面的方式創見多執行緒時,優勢是:

執行緒類只是實現了Runnable介面或Callable介面,還可以繼承其他類。

在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了面向物件的思想。

劣勢是:

程式設計稍微複雜,如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。

使用繼承Thread類的方式建立多執行緒時優勢是:

編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒。

劣勢是:

執行緒類已經繼承了Thread類,所以不能再繼承其他父類。

sleep() 、join()、yield()有什麼區別

1、sleep()方法

在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。 讓其他執行緒有機會繼續執行,但它並不釋放物件鎖。也就是如果有Synchronized同步塊,其他執行緒仍然不能訪問共享資料。注意該方法要捕獲異常

比如有兩個執行緒同時執行(沒有Synchronized),一個執行緒優先順序為MAX_PRIORITY,另一個為MIN_PRIORITY,如果沒有Sleep()方法,只有高優先順序的執行緒執行完成後,低優先順序的執行緒才能執行;但當高優先順序的執行緒sleep(5000)後,低優先順序就有機會執行了。 總之,sleep()可以使低優先順序的執行緒得到執行的機會,當然也可以讓同優先順序、高優先順序的執行緒有執行的機會。

2、yield()方法

yield()方法和sleep()方法類似,也不會釋放“鎖標誌”,區別在於,它沒有引數,即yield()方法只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行,另外yield()方法只能使同優先順序或者高優先順序的執行緒得到執行機會,這也和sleep()方法不同。

3、join()方法

Thread的非靜態方法join()讓一個執行緒B“加入”到另外一個執行緒A的尾部。在A執行完畢之前,B不能工作。

Thread t = new MyThread(); t.start(); t.join();

保證當前執行緒停止執行,直到該執行緒所加入的執行緒完成為止。然而,如果它加入的執行緒沒有存活,則當前執行緒不需要停止。

執行緒池的幾種方式

newFixedThreadPool(int nThreads) 建立一個固定長度的執行緒池,每當提交一個任務就建立一個執行緒,直到達到執行緒池的最大數量,這時執行緒規模將不再變化,當執行緒發生未預期的錯誤而結束時,執行緒池會補充一個新的執行緒

newCachedThreadPool() 建立一個可快取的執行緒池,如果執行緒池的規模超過了處理需求,將自動回收空閒執行緒,而當需求增加時,則可以自動新增新執行緒,執行緒池的規模不存在任何限制

newSingleThreadExecutor() 這是一個單執行緒的Executor,它建立單個工作執行緒來執行任務,如果這個執行緒異常結束,會建立一個新的來替代它;它的特點是能確保依照任務在佇列中的順序來序列執行

newScheduledThreadPool(int corePoolSize) 建立了一個固定長度的執行緒池,而且以延遲或定時的方式來執行任務,類似於Timer。

舉個栗子

private static final Executor exec=Executors.newFixedThreadPool(50);   Runnable runnable=new Runnable(){     public void run(){         ...     } } exec.execute(runnable);   Callable<Object> callable=new Callable<Object>() {     public Object call() throws Exception {         return null;     } };   Future future=executorService.submit(callable); future.get(); // 等待計算完成後,獲取結果 future.isDone(); // 如果任務已完成,則返回 true future.isCancelled(); // 如果在任務正常完成前將其取消,則返回 true future.cancel(true); // 試圖取消對此任務的執行,true中斷執行的任務,false允許正在執行的任務執行完成 參考:

建立執行緒池的幾種方式

執行緒的生命週期

新建(New)、就緒(Runnable)、執行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態

(1)生命週期的五種狀態

新建(new Thread) 當建立Thread類的一個例項(物件)時,此執行緒進入新建狀態(未被啟動)。 例如:Thread t1=new Thread();

就緒(runnable) 執行緒已經被啟動,正在等待被分配給CPU時間片,也就是說此時執行緒正在就緒佇列中排隊等候得到CPU資源。例如:t1.start();

執行(running) 執行緒獲得CPU資源正在執行任務(run()方法),此時除非此執行緒自動放棄CPU資源或者有優先順序更高的執行緒進入,執行緒將一直執行到結束。

死亡(dead) 當執行緒執行完畢或被其它執行緒殺死,執行緒就進入死亡狀態,這時執行緒不可能再進入就緒狀態等待執行。

自然終止:正常執行run()方法後終止

異常終止:呼叫stop()方法讓一個執行緒終止執行

堵塞(blocked) 由於某種原因導致正在執行的執行緒讓出CPU並暫停自己的執行,即進入堵塞狀態。

正在睡眠:用sleep(long t) 方法可使執行緒進入睡眠方式。一個睡眠著的執行緒在指定的時間過去可進入就緒狀態。

正在等待:呼叫wait()方法。(呼叫motify()方法回到就緒狀態)

被另一個執行緒所阻塞:呼叫suspend()方法。(呼叫resume()方法恢復)

參考:

執行緒的生命週期

鎖機制

說說執行緒安全問題

執行緒安全是指要控制多個執行緒對某個資源的有序訪問或修改,而在這些執行緒之間沒有產生衝突。 在Java裡,執行緒安全一般體現在兩個方面: 1、多個thread對同一個java例項的訪問(read和modify)不會相互干擾,它主要體現在關鍵字synchronized。如ArrayList和Vector,HashMap和Hashtable(後者每個方法前都有synchronized關鍵字)。如果你在interator一個List物件時,其它執行緒remove一個element,問題就出現了。 2、每個執行緒都有自己的欄位,而不會在多個執行緒之間共享。它主要體現在java.lang.ThreadLocal類,而沒有Java關鍵字支援,如像static、transient那樣。

樂觀鎖 悲觀鎖 是一種思想。可以用在很多方面。

比如資料庫方面。 悲觀鎖就是for update(鎖定查詢的行) 樂觀鎖就是 version欄位(比較跟上一次的版本號,如果一樣則更新,如果失敗則要重複讀-比較-寫的操作。)

JDK方面: 悲觀鎖就是sync 樂觀鎖就是原子類(內部使用CAS實現)

本質來說,就是悲觀鎖認為總會有人搶我的。 樂觀鎖就認為,基本沒人搶。

CAS 樂觀鎖

樂觀鎖是一種思想,即認為讀多寫少,遇到併發寫的可能性比較低,所以採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。

CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。 CAS頂多算是樂觀鎖寫那一步操作的一種實現方式罷了,不用CAS自己加鎖也是可以的。

ABA 問題

ABA:如果另一個執行緒修改V值假設原來是A,先修改成B,再修改回成A,當前執行緒的CAS操作無法分辨當前V值是否發生過變化。

Java CAS 和ABA問題

樂觀鎖的業務場景及實現方式

樂觀鎖(Optimistic Lock): 每次獲取資料的時候,都不會擔心資料被修改,所以每次獲取資料的時候都不會進行加鎖,但是在更新資料的時候需要判斷該資料是否被別人修改過。如果資料被其他執行緒修改,則不進行資料更新,如果資料沒有被其他執行緒修改,則進行資料更新。由於資料沒有進行加鎖,期間該資料可以被其他執行緒進行讀寫操作。

樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。