第三章、 核心1:實現多線程的正確姿勢

1、實現多線程的方法是1種,2種還是4種?

Oracle官網的文檔是如何寫的?

  • 方法一:實現Runnable接口
  • 方法二:繼承Thread類

1.1 實現示例

  • 實現Runnable接口
/**
 * RunnableStyle
 *
 * @author venlenter
 * @Description: 用Runnable方式創建線程
 * @since unknown, 2020-03-23
 */
public class RunnableStyle implements Runnable {
    @Override
    public void run() {
        System.out.println("用Runnable方法實現線程");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }
}
//輸出結果
用Runnable方法實現線程
  • 繼承Thread類
/**
 * ThreadStyle
 *
 * @author venlenter
 * @Description: 用Thread方法實現線程
 * @since unknown, 2020-03-23
 */
public class ThreadStyle extends Thread {
    @Override
    public void run() {
        System.out.println("用Thread方法實現線程");
    }

    public static void main(String[] args) {
        Thread thread = new ThreadStyle();
        thread.start();
    }
}
//輸出結果
用Thread方法實現線程

1.2 兩種方法的對比

方法1(實現Runnable接口)更好

兩種方法的本質對比

  • (Runnable):最終調用target.run();
實際上執行的是target的run方法
Thread.java
——————————————————————————————————————————
private Runnable target;  //target是我們傳入的new RunnableStyle()
@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
——————————————————————————————————————————
  • (Thread):run()整個都被重寫
調用的是ThreadStyle override的run方法

2、同時使用兩種方法:正確實現方法的總結

  • 實現了Runnable的run方法,但最終被Thread override的run覆蓋,所以只打印了Thread的
/**
 * BothRunnableThread
 *
 * @author venlenter
 * @Description: 同時使用Runnable和Thread
 * @since unknown, 2020-03-23
 */
public class BothRunnableThread {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我來自Runnable");
            }
        }) {
            @Override
            public void run() {
                System.out.println("我來自Thread");
            }
        }.start();
    }
}
//輸出結果
我來自Thread

2.1 總結

通常我們可以分爲2類,Oracle官方文檔也是這樣描述

準確的說,創建線程只有1種方式,那就是構造Thread類,而實現線程的執行單元有2種方式

  • 方法一:實現Runnable接口,重寫run方法,並把Runnable實例傳給Thread類
  • 方法二:重寫Thread的run方法(繼承Thread類)

3、典型錯誤觀點

  • 線程池創建線程也算是一種新建線程的方式(本質也是通過Thread的方式)
/**
 * ThreadPool5
 *
 * @author venlenter
 * @Description: 線程池創建線程的方法
 * @since unknown, 2020-03-24
 */
public class ThreadPool5 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Task(){});
        }
    }
}

class Task implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}
//輸出結果
pool-1-thread-1
pool-1-thread-2
...
pool-1-thread-999
pool-1-thread-1000
  • 通過Callable和FutureTask創建線程,也算是一種新建線程的方式(本質實現了Runnable接口)
  • “無返回值”是實現Runnable接口,“有返回值”是實現callable接口,所以callable是新的實現線程的方式(同上,本質都是實現了Runnable)
  • 定時器(TimerTask implements Runnable)
/**
 * DemoTimmerTask
 *
 * @author venlenter
 * @Description: 定時器創建線程,定時1s打印當前線程名
 * @since unknown, 2020-03-29
 */
public class DemoTimmerTask {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }, 1000, 1000);
    }
}
//輸出結果
Timer-0
Timer-0
Timer-0
  • 匿名內部類
/**
 * AnonymouslnnerClassDemo
 *
 * @author venlenter
 * @Description: 匿名內部類創建線程
 * @since unknown, 2020-03-29
 */
public class AnonymouslnnerClassDemo {
    public static void main(String[] args) {
        //方式1,直接new Thread重寫run方法
        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }.start();
        //方式2,傳入一個Runnable()
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
}
//輸出結果
Thread-0
Thread-1
  • Lambda表達式
/**
 * Lambda
 *
 * @author venlenter
 * @Description: 用lambda方式創建線程
 * @since unknown, 2020-03-29
 */
public class Lambda {
    public static void main(String[] args) {
        new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
    }
}

3.1 總結:多線程的實現方式,在代碼中寫法千變萬化,但其本質萬變不離其宗。就是通過2種方式:繼承Thread類;實現Runnable接口

4、實現多線程常見面試問題

4.1、有多少種實現線程的方法?思路有5點

  1. 從不同的角度看,會有不同的答案
  2. 典型答案是2種(繼承Thread類、實現Runnable接口)
  • 實際上,實現Runnable接口更好一點(有3點優點-見下方4.2)。
  • Runnable方式,最終調用的是傳入的Runnable對象的run()方法
public class RunnableStyle implements Runnable {
    @Override
    public void run() {
        System.out.println("用Runnable方法實現線程");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
		//target.run(),target即new RunnableStyle
        thread.start();
    }
}
  • 而繼承Thread類的方式,是重寫了當前Thread類的run()方法
  1. 但實際上,看原理,兩種本質都是一樣的
Thread.java
private Runnable target;  //target是我們傳入的new RunnableStyle()
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

實際上都是利用了Thread類的run方法。只不過一個是重寫了Thread類的run方法,一個是傳進來target對象,再去執行target的run方法

  1. 其實還有其他創建方式,如線程池、定時器等方式,但本質上還是使用了上面的2種方式
  2. 總的來說:本質只有一種,新建線程必須通過Thread類,但通常我們把它區分爲2種形式(一種是繼承Thread類,一種是實現Runnable接口),另外還有更多的表現形式(如線程池、定時器、匿名內部類、Lambda)

4.2、實現Runnable接口和繼承Thread類哪種方式更好

  1. 從代碼架構角度
  • 這裏有2件事情,第一是具體的功能,即run方法;
  • 第二是跟線程生命週期相關的(創建線程、運行線程),這個實際上是Thread類纔去做的事情,從代碼設計上來說,應該解耦出來,所以用Runnable接口的方式更好
  1. 新建線程的損耗
  • 如果用Thread的方式,每次需要new一個Thread對象,新建一個線程,執行完還需要銷燬
  • 如果用Runnable的方式,傳入實現Runnable的對象,就可以反覆使用這個線程,線程池就是這樣做的,這樣用於生命週期的損耗就減少了
  1. Java不支持雙繼承

筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章