熟悉Java的人都能很容易地寫出以下代碼:java
public static class MyThread extends Thread { <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override public void run() { System.out.println("MyThread is running..."); } } public static void main(String[] args) { Thread t = new MyThread(); t.start(); }</a>
這是一個面試常問的基礎問題,你應該確定的回答線程只有五種狀態,分別是:新建狀態、就緒狀態、執行狀態、阻塞狀態、終止狀態。程序員
因爲Scheduler(調度器)的時間片分配算法,每一個Running的線程會執行多長時間是未知的,所以線程可以在Runnable和Running之間來回轉換。阻塞狀態的線程必須先進入就緒狀態才能進入執行狀態。面試
Running線程在主動調用Thread.sleep()、obj.wait()、thread.join()時會進入TIMED-WAITING或WAITING狀態並主動讓出CPU執行權。若是是TIMED-WAITING,那麼在通過必定的時間以後會主動返回並進入Runnable狀態等待時間片的分配。算法
thread.join()的底層就是當前線程不斷輪詢thread是否存活,若是存活就不斷地wait(0)。編程
Running線程在執行過程當中若是遇到了臨界區(synchronized修飾的方法或代碼塊)而且須要獲取的鎖正在被其餘線程佔用,那麼他會主動將本身掛起並進入BLOCKED狀態。安全
若是持有鎖的線程退出臨界區,那麼在該鎖上等待的線程都會被喚醒並進入就緒狀態,但只有搶到鎖的線程會進入執行狀態,其餘沒有搶到鎖的線程仍將進入阻塞狀態。多線程
若是某個線程調用了obj的notify/notifyAll方法,那麼在該線程退出臨界區時(調用wait/notify必須先經過synchronized獲取對象的鎖),被喚醒的等待在obj.wait上的線程纔會從阻塞狀態進入就緒狀態獲取obj的monitor,而且只有搶到monitor的線程纔會從obj.wait返回,而沒有搶到的線程仍舊會阻塞在obj.wait上架構
在執行狀態下的線程執行完run方法或阻塞狀態下的線程被interrupt時會進入終止狀態,隨後會被銷燬。併發
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) {} } } private native void start0();
start方法主要作了三件事:app
咱們將經過Thread來模擬這樣一個場景:銀行多窗口叫號。從而思考已經有Thread了爲何還要引入Runnable
首先咱們須要一個窗口線程模擬叫號(窗口叫號,相應號碼的顧客到對應窗口辦理業務)的過程:
public class TicketWindow extends Thread { public static final Random RANDOM = new Random(System.currentTimeMillis()); private static final int MAX = 20; private int counter; private String windowName; public TicketWindow(String windowName) { super(windowName); counter = 0; this.windowName = windowName; } <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override public void run() { System.out.println(windowName + " start working..."); while (counter < MAX){ System.out.println(windowName + ": It's the turn to number " + counter++); //simulate handle the business try { Thread.sleep(RANDOM.nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } } }</a>
而後編寫一個叫號客戶端模擬四個窗口同時叫號:
public class WindowThreadClient { public static void main(String[] args) { Stream.of("Window-1","Window-2","Window-3","Window-4").forEach( windowName -> new TicketWindow(windowName).start() ); } }
你會發現同一個號碼被叫了四次,顯然這不是咱們想要的。正常狀況下應該是四個窗口共享一個叫號系統,窗口只負責辦理業務而叫號則應該交給叫號系統,這是典型的OOP中的單一職責原則。
咱們將線程和要執行的任務耦合在了一塊兒,所以出現瞭如上所述的尷尬狀況。線程的職責就是執行任務,它有它本身的運行時狀態,咱們不該該將要執行的任務的相關狀態(如本例中的counter、windowName)將線程耦合在一塊兒,而應該將業務邏輯單獨抽取出來做爲一個邏輯執行單元,當須要執行時提交給線程便可。因而就有了Runnable接口:
public interface Runnable { public abstract void run(); }
所以咱們能夠將以前的多窗口叫號改造一下:
public class TicketWindowRunnable implements Runnable { public static final Random RANDOM = new Random(System.currentTimeMillis()); private static final int MAX = 20; private int counter = 0; <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override public void run() { System.out.println(Thread.currentThread().getName() + " start working..."); while (counter < MAX){ System.out.println(Thread.currentThread().getName()+ ": It's the turn to number " + counter++); //simulate handle the business try { Thread.sleep(RANDOM.nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } } }</a>
測試類:
public class WindowThreadClient { public static void main(String[] args) { TicketWindowRunnable ticketWindow = new TicketWindowRunnable(); Stream.of("Window-1", "Window-2", "Window-3", "Window-4").forEach( windowName -> new Thread(ticketWindow, windowName).start() ); } }
如此你會發現沒有重複的叫號了。可是這個程序並非線程安全的,由於有多個線程同時更改windowRunnable中的counter變量,因爲本節主要闡述Runnable的做用,所以暫時不對此展開討論。
將Thread中的run經過接口的方式暴露出來還有一個好處就是對策略模式和函數式編程友好。
首先簡單介紹一下策略模式,假設咱們如今須要計算一個員工的我的所得稅,因而咱們寫了以下工具類,傳入基本工資和獎金便可調用calculate得出應納稅額:
public class TaxCalculator { private double salary; private double bonus; public TaxCalculator(double base, double bonus) { this.salary = base; this.bonus = bonus; } public double calculate() { return salary * 0.03 + bonus * 0.1; } }
這樣寫有什麼問題?咱們將應納稅額的計算寫死了:salary 0.03 + bonus 0.1,而稅率並不是一層不變的,客戶提出需求變更也是常有的事!難道每次需求變動咱們都要手動更改這部分代碼嗎?
這時策略模式來幫忙:當咱們的需求的輸入是不變的,但輸出須要根據不一樣的策略作出相應的調整時,咱們能夠將這部分的邏輯抽取成一個接口:
public interface TaxCalculateStrategy { public double calculate(double salary, double bonus); }
具體策略實現:
public class SimpleTaxCalculateStrategy implements TaxCalculateStrategy { <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override public double calculate(double salary, double bonus) { return salary * 0.03 + bonus * 0.1; } }</a>
而業務代碼僅調用接口:
public class TaxCalculator { private double salary; private double bonus; private TaxCalculateStrategy taxCalculateStrategy; public TaxCalculator(double base, double bonus, TaxCalculateStrategy taxCalculateStrategy) { this.salary = base; this.bonus = bonus; this.taxCalculateStrategy = taxCalculateStrategy; } public double calculate() { return taxCalculateStrategy.calculate(salary, bonus); } }
將Thread中的邏輯執行單元run抽取成一個接口Runnable有着殊途同歸之妙。由於實際業務中,須要提交給線程執行的任務咱們是沒法預料的,抽取成一個接口以後就給咱們的應用程序帶來了很大的靈活性。
另外在JDK1.8中引入了函數式編程和lambda表達式,使用策略模式對這個特性也是很友好的。仍是藉助上面這個例子,若是計算規則變成了(salary + bonus) * 1.5,可能咱們須要新增一個策略類:
public class AnotherTaxCalculatorStrategy implements TaxCalculateStrategy { <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override public double calculate(double salary, double bonus) { return (salary + bonus) * 1.5; } }</a>
在JDK增長內部類語法糖以後,可使用匿名內部類省去建立新類的開銷:
public class TaxCalculateTest { public static void main(String[] args) { TaxCalculator taxCalaculator = new TaxCalculator(5000,1500, new TaxCalculateStrategy(){ <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override public double calculate(double salary, double bonus) { return (salary + bonus) * 1.5; } }); } }</a>
可是在JDK新增函數式編程後,能夠更加簡潔明瞭:
public class TaxCalculateTest { public static void main(String[] args) { TaxCalculator taxCalaculator = new TaxCalculator(5000, 1500, (salary, bonus) -> (salary + bonus) * 1.5); } }
這對只有一個抽象方法run的Runnable接口來講是一樣適用的。
查看Thread的構造方法,追溯到init方法(略有刪減):
Thread parent = currentThread(); if (g == null) { if (g == null) { g = parent.getThreadGroup(); } } this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); this.target = target; setPriority(priority); this.stackSize = stackSize; tid = nextThreadID();
g是當前對象的ThreadGroup,2~8就是在設置當前對象所屬的線程組,若是在new Thread時沒有顯式指定,那麼默認將父線程(當前執行new Thread的線程)線程組設置爲本身的線程組。
經過nextThreadID會發現是一個static synchronized方法,原子地取得線程序列號threadSeqNumber自增後的值:
public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getId()); //11 }).start(); }
爲何main中建立的第一個線程的ID是11(意味着他是JVM啓動後建立的第11個線程)呢?這由於在JVM在執行main時會啓動JVM進程的第一個線程(叫作main線程),而且會啓動一些守護線程,好比GC線程。
這裏要注意的是每一個線程都有一個私有的虛擬機棧。全部線程的棧都存放在JVM運行時數據區域的虛擬機棧區域中。
Thread提供了一個能夠設置stackSize的重載構造方法:
public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
官方文檔對該參數的描述以下:
The stack size is the approximate number of bytes of address space that the virtual machine is to allocate for this thread's stack. The effect of the stackSize parameter, if any, is highly platform dependent.
你能經過指定stackSize參數近似地指定虛擬機棧的內存大小(注意:是內存大小即字節數而不是棧中所能容納的最大棧幀數目,並且這個大小指的是該線程的棧大小而並不是是整個虛擬機棧區的大小)。且該參數具備高度的平臺依賴性,也就是說在各個操做系統上,一樣的參數表現出來的效果有所不一樣。
On some platforms, specifying a higher value for thestackSizeparameter may allow a thread to achieve greater recursion depth before throwing a StackOverflowError. Similarly, specifying a lower value may allow a greater number of threads to exist concurrently without throwing an OutOfMemoryError (or other internal error). The details of the relationship between the value of thestackSizeparameter and the maximum recursion depth and concurrency level are platform-dependent. On some platforms, the value of the stackSize parameter may have no effect whatsoever.
在一些平臺上,爲stackSize指定一個較大的值,可以容許線程在拋出棧溢出異常前達到較大的遞歸深度(由於方法棧幀的大小在編譯期可知,以局部變量表爲例,基本類型變量中只有long和double佔8個字節,其他的做4個字節處理,引用類型根據虛擬機是32位仍是64位而佔4個字節或8個字節。如此的話棧越大,棧所能容納的最大棧幀數目也即遞歸深度也就越大)。相似的,指定一個較小的stackSize可以讓更多的線程共存而避免OOM異常(有的讀者可能會異或,棧較小怎麼還不容易拋出OOM異常了呢?不是應該棧較小,內存更不夠用,更容易OOM嗎?其實單線程環境下,只可能發生棧溢出而不會發生OOM,由於每一個方法對應的棧幀大小在編譯器就可知了,線程啓動時會從虛擬機棧區劃分一塊內存做爲棧的大小,所以不管是壓入的棧幀太多仍是將要壓入的棧幀太大都只會致使棧沒法繼續容納棧幀而拋出棧溢出。那麼何時回拋出OOM呢。對於虛擬機棧區來講,若是沒有足夠的內存劃分出來做爲新建線程的棧內存時,就會拋出OOM了。這就不難理解了,有限的進程內存除去堆內存、方法區、JVM自身所需內存以後剩下的虛擬機棧是有限的,分配給每一個棧的越少,可以並存的線程天然就越多了)。最後,在一些平臺上,不管將stackSize設置爲多大均可能不會起到任何做用。
The virtual machine is free to treat thestackSizeparameter as a suggestion. If the specified value is unreasonably low for the platform, the virtual machine may instead use some platform-specific minimum value; if the specified value is unreasonably high, the virtual machine may instead use some platform-specific maximum. Likewise, the virtual machine is free to round the specified value up or down as it sees fit (or to ignore it completely).
虛擬機會將stackSize視爲一種建議,在棧大小的設置上仍有必定的話語權。若是給定的值過小,虛擬機會將棧大小設置爲平臺對應的最小棧大小;相應的若是給定的值太大,則會設置成平臺對應的最大棧大小。又或者,虛擬機可以按照給定的值向上或向下取捨以設置一個合適的棧大小(甚至虛擬機會忽略它)。
Due to the platform-dependent nature of the behavior of this constructor, extreme care should be exercised in its use. The thread stack size necessary to perform a given computation will likely vary from one JRE implementation to another. In light of this variation, careful tuning of the stack size parameter may be required, and the tuning may need to be repeated for each JRE implementation on which an application is to run.
因爲此構造函數的平臺依賴特性,在使用時須要格外當心。線程棧的實際大小的計算規則會由於JVM的不一樣實現而有不一樣的表現。鑑於這種變化,可能須要仔細調整堆棧大小參數,而且對於應用程序使用的不一樣的JVM實現須要有不一樣的調整。
Implementation note: Java platform implementers are encouraged to document their implementation's behavior with respect to thestackSizeparameter.
針對於上面所涉及到的知識點我總結出了有1到5年開發經驗的程序員在面試中涉及到的絕大部分架構面試題及答案作成了文檔和架構視頻資料免費分享給你們(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術資料),但願能幫助到您面試前的複習且找到一個好的工做,也節省你們在網上搜索資料的時間來學習,也能夠關注我一下之後會有更多幹貨分享。