Java多線程之Runnable與Thread


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的實現機制再來談談這個問題

  首先 ThreadRunnable 實際上是一種靜態代理的實現方式。我們可以簡單看一下源代碼就了解了:

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() 方法會調用 nativestart0()方法,繼而由 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 }
View Code

  我們運行這個程序會發現,三個線程各買了五張票,總共買了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 }
View Code

  我們運行這個程序發現,三個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】。

轉載請注明出處和鏈接地址,歡迎轉載,謝謝!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM