Java多線程之Thread與Runnable
一、Thread VS Runnable
在java中可有兩種方式實現多線程,一種是繼承Thread類,一種是實現Runnable接口;Thread類和Runnable接口都是在java.lang包中定義的。接下來本文給大家介紹下Java中Runnable和Thread的區別,當然啦,也算做是我整理的學習筆記吧,一起看看吧
- 實現Runnable接口方式可以避免繼承Thread方式由於Java單繼承特性帶來的缺陷。具體什么缺陷呢?
①首先來從接口實現和類繼承的區別來談談
如果你想寫一個類C,但這個類C已經繼承了一個類A,此時,你又想讓C實現多線程。用繼承Thread類的方式不行了。(因為單繼承的局限性),此時,只能用Runnable接口,Runnable接口就是為了解決這種情境出現的
②從Thread和Runnable的實現機制再來談談這個問題
首先 Thread
和 Runnable
實際上是一種靜態代理的實現方式。我們可以簡單看一下源代碼就了解了:
public interface Runnable { public abstract void run(); } public class Thread implements Runnable { ... private Runnable target; public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } public void run() { if (target != null) { target.run(); //代理的target的run方法 } } }
另外一個我們知道線程啟動是調用 thread.start()
方法,但是 start()
方法會調用 native
的 start0()
方法,繼而由 JVM
來實現多線程的控制,因為需要系統調用來控制時間分片。
現在我們可以深入理解一下兩種線程實現方式的異同。
Class MyThread extends Thread(){ public int count = 10; public synchronized void run(){ while(count>0){ count--; } } } new Mythread().start(); //啟動 n 個線程 new Mythread().start();
這種實現方式實際上是重寫了 run()
方法,由於線程的資源和 Thread
實例捆綁在一起,所以不同的線程的資源不會進行共享。
Class MyThread implements Runnable{ public int count = 10; public synchronized void run(){ while(count>0){ count--; } } } MyThread mt = new MyThread(); new Thread(mt).start(); //啟動 n 個線程 new Thread(mt).start();
這種實現方式就是靜態代理的方式,線程資源與 Runable
實例捆綁在一起,Thread
只是作為一個代理類,所以資源可以進行共享。
③從 Java
語言設計者的角度來看
Runnable
可以理解為 Task
,對應的是具體的要運行的任務,而 Thread
對應某一個具體的線程運行的載體。綜上,繼承 Thread
來實現,可以說是不推薦的。
實現Runnable的代碼可以被多個線程(Thread實例)共享,適合於多個線程處理同一資源的情況。
下面以典型的買票程序來說明這點(這里我為了讓大家理解,沒有用synchronized同步代碼塊,可能運行結果會不按正常出牌):
①通過繼承Thread來實現

1 package me.demo.thread; 2 3 class MyThread extends Thread { 4 private int ticketsCont = 5;// 一共有五張火車票 5 private String name; // 窗口,也即是線程的名字 6 7 public MyThread(String name) { 8 this.name = name; 9 } 10 11 @Override 12 public void run() { 13 while (ticketsCont > 0) { 14 ticketsCont--; // 如果還有票就賣一張 15 System.out.println(name + "賣了一張票,剩余票數為:" + ticketsCont); 16 } 17 } 18 } 19 20 public class TicketsThread { 21 22 public static void main(String[] args) { 23 // 創建三個線程,模擬三個窗口賣票 24 MyThread mt1 = new MyThread("窗口1"); 25 MyThread mt2 = new MyThread("窗口2"); 26 MyThread mt3 = new MyThread("窗口3"); 27 28 // 啟動這三個線程,也即是窗口,開始賣票 29 mt1.start(); 30 mt2.start(); 31 mt3.start(); 32 } 33 }
我們運行這個程序會發現,三個線程各買了五張票,總共買了15張票,而我們模擬的火車站窗口總共就只剩下五張票,這顯然會出問題的啊,更別談資源共享了,因為程序中,我們創建了三個MyThread的實例對象,而作為普通成員變量的ticketsCont(注意,這里是非靜態成員變量,如果是靜態成員變量那就另當別論了)顯然被初始化了三次,存在於三個不同的對象中,所以也就造成了這個結果,可見Thread不適合資源共享。
②通過實現Runnable接口來實現:

1 package me.demo.runnable; 2 3 class MyThread implements Runnable { 4 5 private int ticketsCont = 5; // 一共有五張火車票 6 7 @Override 8 public void run() { 9 while (ticketsCont > 0) { 10 ticketsCont--;// 如果有票就賣掉一張 11 System.out.println(Thread.currentThread().getName() + "賣了一張票,剩余票數為:" + ticketsCont); 12 } 13 } 14 15 } 16 17 public class TicketsRunnable { 18 19 public static void main(String[] args) { 20 MyThread thread = new MyThread(); 21 // 創建三個線程來模擬三個售票窗口 22 Thread th1 = new Thread(thread, "窗口1"); 23 Thread th2 = new Thread(thread, "窗口2"); 24 Thread th3 = new Thread(thread, "窗口3"); 25 26 // 啟動這三個線程,也即是三個窗口開始賣票 27 th1.start(); 28 th2.start(); 29 th3.start(); 30 } 31 }
我們運行這個程序發現,三個Thread總共賣了五張票,這顯然符合日常生活中的情況,因為Thread共享了實現了Runnable接口的MyThread類的實例中的成員變量ticketsCont,也就不存在上述問題了。
另外,針對以上代碼補充三點:
-
在第二種方法(Runnable)中,ticket輸出的順序並不是54321,這是因為線程執行的時機難以預測,ticket--並不是原子操作。
-
在第一種方法中,我們new了3個Thread對象,即三個線程分別執行三個對象中的代碼,因此便是三個線程去獨立地完成賣票的任務;而在第二種方法中,我們同樣也new了3個Thread對象,但只有一個Runnable對象,3個Thread對象共享這個Runnable對象中的代碼,因此,便會出現3個線程共同完成賣票任務的結果。如果我們new出3個Runnable對象,作為參數分別傳入3個Thread對象中,那么3個線程便會獨立執行各自Runnable對象中的代碼,即3個線程各自賣5張票。
-
在第二種方法中,由於3個Thread對象共同執行一個Runnable對象中的代碼,因此可能會造成線程的不安全,比如可能ticket會輸出-1(如果我們System.out....語句前加上線程休眠操作,該情況將很有可能出現),這種情況的出現是由於,一個線程在判斷ticket為1>0后,還沒有來得及減1,另一個線程已經將ticket減1,變為了0,那么接下來之前的線程再將ticket減1,便得到了-1。這就需要加入同步操作(即互斥鎖),確保同一時刻只有一個線程在執行每次for循環中的操作。而在第一種方法中,並不需要加入同步操作,因為每個線程執行自己Thread對象中的代碼,不存在多個線程共同執行同一個方法的情況。
二、總結
Thread類也是Runnable接口的子類,可見, 實現Runnable接口相對於繼承Thread類來說,有如下顯著的好處:
-
適合多個相同程序代碼的線程去處理同一資源的情況,把虛擬CPU(線程)同程序的代碼,數據有效的分離,較好地體現了面向對象的設計思想。
-
可以避免由於Java的單繼承特性帶來的局限。我們經常碰到這樣一種情況,即當我們要將已經繼承了某一個類的子類放入多線程中,由於一個類不能同時有兩個父類,所以不能用繼承Thread類的方式,那么,這個類就只能采用實現Runnable接口的方式了。
-
有利於程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的。當多個線程的執行代碼來自同一個類的實例時,即稱它們共享相同的代碼。多個線程操作相同的數據,與它們的代碼無關。當共享訪問相同的對象是,即它們共享相同的數據。當線程被構造時,需要的代碼和數據通過一個對象作為構造函數實參傳遞進去,這個對象就是一個實現了Runnable接口的類的實例。
參考文章:
https://segmentfault.com/q/1010000006056386
http://www.jb51.net/article/105487.htm
https://www.cnblogs.com/lt132024/p/6438750.html
如果,您對我的這篇博文有什么疑問,歡迎評論區留言,大家互相討論學習。
如果,您認為閱讀這篇博客讓您有些收獲,不妨點擊一下右下角的【推薦】。
如果,您希望更容易地發現我的新博客,不妨點擊一下左下角的【關注我】。
如果,您對我的博文感興趣,可以關注我的后續博客,我是【AlbertRui】。轉載請注明出處和鏈接地址,歡迎轉載,謝謝!