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): 每次獲取資料的時候,都不會擔心資料被修改,所以每次獲取資料的時候都不會進行加鎖,但是在更新資料的時候需要判斷該資料是否被別人修改過。如果資料被其他執行緒修改,則不進行資料更新,如果資料沒有被其他執行緒修改,則進行資料更新。由於資料沒有進行加鎖,期間該資料可以被其他執行緒進行讀寫操作。
樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。