從線程到多線程超詳細講解(內存圖解)

先上個圖,內存圖

        內存區域中的虛擬機棧和本地方法棧在每個線程中都各自有一份,而方法區和堆在進程中一直有一份,多個線程共享這些資源。

言歸正傳

1.線程概念

先搞清楚三個概念:

  • 程序(program)是爲完成特定任務、用某種語言編寫的一組指令的集合。即指一
    段靜態的代碼,靜態對象。
  • 進程(process)是程序的一次執行過程,或是正在運行的一個程序。是一個動態
    的過程:有它自身的產生、存在和消亡的過程。——生命週期
    ■如:運行中的QQ,運行中的MP3播放器
    ■程序是靜態的,進程是動態的
    ■進程作爲資源分配的單位,系統在運行時會爲每個進程分配不同的內存區域
  •  線程(thread),進程可進一步細化爲線程,是一個程序內部的一條執行路徑
    ■若一個進程同一時間 並行執行多個線程,就是支持多線程的
    ■線程作爲調度和執行的單位,每個線程擁有獨立的運行棧和程序計數器(pc),線程切換的開
    銷小

注意:一個進程中的多個線程共享相同的內存單元/內存地址空間它們從同一堆中分配對象,可以
訪問相同的變量和對象。這就使得線程間通信更簡便、高效。但多個線程操作共享的系統資
源可能就會帶來安全的隱患

2.多線程的優點

背景 : 以單核CPU爲例,只使用單個線程先後完成多個任務(調用多個方法),肯定比用多個線程來完成用的時間更短,那爲什麼還用多線程呢?

多線程程序的優點:

  1. 提高應用程序的響應。對圖形化界面更有意義,可增強用戶體驗。
  2. 提高計算機系統CPU的利用率
  3. 改善程序結構。將既長又複雜的進程分爲多個線程,獨立運行,利於理解和修改。

3.創建多線程

方式一:繼承Thread

多線程的交替執行在下面這段代碼裏也有很好的展現。

/**
 * 多線程的創建,方式一:繼承於Thread類
 * 1. 創建一個繼承於Thread類的子類
 * 2. 重寫Thread類的run() --> 將此線程執行的操作聲明在run()中
 * 3. 創建Thread類的子類的對象
 * 4. 通過此對象調用start()
 * <p>
 * 例子:遍歷100以內的所有的偶數
 *
 * @author wudi
 * @create 2019-02-13 上午 11:46
 */

//1. 創建一個繼承於Thread類的子類
class MyThread extends Thread {
    //2. 重寫Thread類的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}


public class ThreadTest {
    public static void main(String[] args) {
        //3. 創建Thread類的子類的對象
        MyThread t1 = new MyThread();

        //4.通過此對象調用start():①啓動當前線程 ② 調用當前線程的run()
        t1.start();
        //問題一:我們不能通過直接調用run()的方式啓動線程。
//        t1.run();

        //問題二:再啓動一個線程,遍歷100以內的偶數。不可以還讓已經start()的線程去執行。會報IllegalThreadStateException
//        t1.start();
        //我們需要重新創建一個線程的對象
        MyThread t2 = new MyThread();
        t2.start();


        //如下操作仍然是在main線程中執行的。
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
            }
        }
    }

}

注意:

  • 如“問題一”所說,“t1.run()”只是單純的對象調用方法而已,並沒有去啓動一個新的線程。必須調用star()方法才能啓動一個新的線程並調用重寫的run()方法。
  • 如“問題二”所說,不能用創建的thread對象重複調用start()方法另啓動一個新的線程。再啓動一個新線程需要重新再創建一個Thread()對象。

方式二:實現Runable

package atguigu.java;

/**
 * 創建多線程的方式二:實現Runnable接口
 * 1. 創建一個實現了Runnable接口的類
 * 2. 實現類去實現Runnable中的抽象方法:run()
 * 3. 創建實現類的對象
 * 4. 將此對象作爲參數傳遞到Thread類的構造器中,創建Thread類的對象
 * 5. 通過Thread類的對象調用start()
 *
 *
 * 比較創建線程的兩種方式。
 * 開發中:優先選擇:實現Runnable接口的方式
 * 原因:1. 實現的方式沒有類的單繼承性的侷限性
 *      2. 實現的方式更適合來處理多個線程有共享數據的情況。
 *
 * 聯繫:public class Thread implements Runnable
 * 相同點:兩種方式都需要重寫run(),將線程要執行的邏輯聲明在run()中。
 *
 * @author wudi
 * @create 2019-02-13 下午 4:34
 */
//1. 創建一個實現了Runnable接口的類
class MThread implements Runnable{

    //2. 實現類去實現Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}


public class ThreadTest1 {
    public static void main(String[] args) {
        //3. 創建實現類的對象
        MThread mThread = new MThread();
        //4. 將此對象作爲參數傳遞到Thread類的構造器中,創建Thread類的對象
        Thread t1 = new Thread(mThread);
        t1.setName("線程1");
        //5. 通過Thread類的對象調用start():① 啓動線程 ②調用當前線程的run()-->調用了Runnable類型的target的run()
        t1.start();

        //再啓動一個線程,遍歷100以內的偶數
        Thread t2 = new Thread(mThread);
        t2.setName("線程2");
        t2.start();
    }

}

 比較創建線程的兩種方式:
 開發中:優先選擇:實現Runnable接口的方式
 原因:1. 實現的方式沒有類的單繼承性的侷限性              2. 實現的方式更適合來處理多個線程有共享數據的情況。
 聯繫:public class Thread implements Runnable
 相同點:兩種方式都需要重寫run(),將線程要執行的邏輯聲明在run()中。

4.線程常用的方法及優先級

package atguigu.java;

/**
 * 測試Thread中的常用方法:
 * 1. start():啓動當前線程;調用當前線程的run()
 * 2. run(): 通常需要重寫Thread類中的此方法,將創建的線程要執行的操作聲明在此方法中
 * 3. currentThread():靜態方法,返回執行當前代碼的線程
 * 4. getName():獲取當前線程的名字
 * 5. setName():設置當前線程的名字
 * 6. yield():釋放當前cpu的執行權
 * 7. join():在線程a中調用線程b的join(),此時線程a就進入阻塞狀態,直到線程b完全執行完以後,線程a才
 *           結束阻塞狀態。
 * 8. stop():已過時。當執行此方法時,強制結束當前線程。
 * 9. sleep(long millitime):讓當前線程“睡眠”指定的millitime毫秒。在指定的millitime毫秒時間內,當前
 *                          線程是阻塞狀態。
 * 10. isAlive():判斷當前線程是否存活
 *
 *
 * 線程的優先級:
 * 1.
 * MAX_PRIORITY:10
 * MIN _PRIORITY:1
 * NORM_PRIORITY:5  -->默認優先級
 * 2.如何獲取和設置當前線程的優先級:
 *   getPriority():獲取線程的優先級
 *   setPriority(int p):設置線程的優先級
 *
 *   說明:高優先級的線程要搶佔低優先級線程cpu的執行權。但是隻是從概率上講,高優先級的線程高概率的情況下
 *   被執行。並不意味着只有當高優先級的線程執行完以後,低優先級的線程才執行。
 *
 *
 * @author wudi
 * @create 2019-02-13 下午 2:26
 */
class HelloThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){

//                try {
//                    sleep(10);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }

                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }

//            if(i % 20 == 0){
//                yield();
//            }

        }

    }

    public HelloThread(String name){
        super(name);
    }
}


public class ThreadMethodTest {
    public static void main(String[] args) {

        HelloThread h1 = new HelloThread("Thread:1");

//        h1.setName("線程一");
        //設置分線程的優先級
        h1.setPriority(Thread.MAX_PRIORITY);

        h1.start();

        //給主線程命名
        Thread.currentThread().setName("主線程");
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }

//            if(i == 20){
//                try {
//                    h1.join();
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//            }

        }

//        System.out.println(h1.isAlive());

    }
}

關於yield和sleep兩個方法的區別:https://blog.csdn.net/qq_38261174/article/details/81364616         這篇文章講解的很清楚了。

5.線程的生命週期

一張圖全部明白,簡單直觀(面試中常問的一個問題)

6.線程的安全問題

上面說過了線程實現的兩種方式:

         繼承Thread                                                    實現Runable

因爲多線程同時操作共享數據時可能會出現髒讀,不可重複讀和幻讀等問題。這就是線程安全問題。一般有兩種解決辦法:

        同步代碼塊處理                                             同步方法處理

顯然,兩兩組合就有四種情況:

  • 同步代碼塊處理實現Runable的線程安全問題
package com.atguigu.java;

/**
 * 例子:創建三個窗口賣票,總票數爲100張.使用實現Runnable接口的方式
 *
 * 1.問題:賣票過程中,出現了重票、錯票 -->出現了線程的安全問題
 * 2.問題出現的原因:當某個線程操作車票的過程中,尚未操作完成時,其他線程參與進來,也操作車票。
 * 3.如何解決:當一個線程a在操作ticket的時候,其他線程不能參與進來。直到線程a操作完ticket時,其他
 *            線程纔可以開始操作ticket。這種情況即使線程a出現了阻塞,也不能被改變。
 *
 *
 * 4.在Java中,我們通過同步機制,來解決線程的安全問題。
 *
 *  方式一:同步代碼塊
 *
 *   synchronized(同步監視器){
 *      //需要被同步的代碼
 *
 *   }
 *  說明:1.操作共享數據的代碼,即爲需要被同步的代碼。  -->不能包含代碼多了,也不能包含代碼少了。
 *       2.共享數據:多個線程共同操作的變量。比如:ticket就是共享數據。
 *       3.同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖。(一定要保證這個對象是唯一的)
 *          要求:多個線程必須要共用同一把鎖。
 *
 *       補充:在實現Runnable接口創建多線程的方式中,我們可以考慮使用this充當同步監視器。
 *  方式二:同步方法。
 *     如果操作共享數據的代碼完整的聲明在一個方法中,我們不妨將此方法聲明同步的。
 *
 *
 *  5.同步的方式,解決了線程的安全問題。---好處
 *    操作同步代碼時,只能有一個線程參與,其他線程等待。相當於是一個單線程的過程,效率低。 ---侷限性
 *
 * @author 吳迪
 * @create 2019-02-13 下午 4:47
 */
class Window1 implements Runnable{

    private int ticket = 100;
//    Object obj = new Object();
//    Dog dog = new Dog();
    @Override
    public void run() {
//        Object obj = new Object();
        while(true){
            synchronized (this){//此時的this:唯一的Window1的對象   //方式二:synchronized (dog) {

                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);


                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}


class Dog{

}
  • 同步代碼塊處理繼承Thread的線程安全問題
package com.atguigu.java;

/**
 * @author wudi
 * @create 2019-02-15 上午 11:15
 */
/**
 * 使用同步代碼塊解決繼承Thread類的方式的線程安全問題
 *
 * 例子:創建三個窗口賣票,總票數爲100張.使用繼承Thread類的方式
 *
 * 說明:在繼承Thread類創建多線程的方式中,慎用this充當同步監視器,考慮使用當前類充當同步監視器。
 *
 * @author 吳迪
 * @create 2019-02-13 下午 4:20
 */
class Window2 extends Thread{


    private static int ticket = 100;

    private static Object obj = new Object();

    @Override
    public void run() {

        while(true){
            //正確的
//            synchronized (obj){
            synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只會加載一次
                //錯誤的方式:this代表着t1,t2,t3三個對象
//              synchronized (this){

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(getName() + ":賣票,票號爲:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }

        }

    }
}


public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();


        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}
  • 同步方法處理實現Runable的線程安全問題
package com.atguigu.java;

/**
 * 使用同步方法解決實現Runnable接口的線程安全問題
 *
 *
 *  關於同步方法的總結:
 *  1. 同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。
 *  2. 非靜態的同步方法,同步監視器是:this
 *     靜態的同步方法,同步監視器是:當前類本身
 *
 * @author wudi
 * @create 2019-02-15 上午 11:35
 */


class Window3 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {

            show();
        }
    }

    private synchronized void show(){//同步監視器:this
        //synchronized (this){

            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);

                ticket--;
            }
        //}
    }
}


public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}
  • 同步方法處理繼承Thread的線程安全問題
package com.atguigu.java;

/**
 * 使用同步方法處理繼承Thread類的方式中的線程安全問題
 *
 * @author shkstart
 * @create 2019-02-15 上午 11:43
 */
class Window4 extends Thread {


    private static int ticket = 100;

    @Override
    public void run() {

        while (true) {

            show();
        }

    }
    private static synchronized void show(){//同步監視器:Window4.class
        //private synchronized void show(){ //同步監視器:t1,t2,t3。此種解決方式是錯誤的
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);
            ticket--;
        }
    }
}


public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();


        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

  總結: 通過這些例子可以很明顯的感覺到還是實現Runable的方式更好用。比較在上面也提到過,在線程安全這塊感覺更明顯,實現Runable的方式在考慮鎖對象的時候更容易選擇,不用考慮那麼多,直接用this一般都不會錯。而用Thread的時候就要考慮當前對象是不是唯一的,否則還會出現安全問題。

7.死鎖問題

  • 死鎖
  1. 不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
  2. 出現死鎖後,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續
  • 解決方法
  1. 專門的算法、原則
  2. 儘量減少同步資源的定義
  3. 儘量避免嵌套同步
  • 代碼演示
package com.atguigu.java1;

/**
 * 演示線程的死鎖問題
 *
 * 1.死鎖的理解:不同的線程分別佔用對方需要的同步資源不放棄,
 * 都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
 *
 * 2.說明:
 * 1)出現死鎖後,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續
 * 2)我們使用同步時,要避免出現死鎖。
 *
 * @author wudi
 * @create 2019-02-15 下午 3:20
 */
public class ThreadTest {

    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();


        new Thread(){
            @Override
            public void run() {

                synchronized (s1){

                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }


                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }


                }

            }
        }.start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){

                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }


                }



            }
        }).start();


    }


}

 

package com.atguigu.java1;
//死鎖的演示
class A {
	public synchronized void foo(B b) { //同步監視器:A類的對象:a
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 進入了A實例的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 企圖調用B實例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步監視器:A類的對象:a
		System.out.println("進入了A類的last方法內部");
	}
}

class B {
	public synchronized void bar(A a) {//同步監視器:b
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 進入了B實例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 企圖調用A實例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步監視器:b
		System.out.println("進入了B類的last方法內部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B(); 

	public void init() {
		Thread.currentThread().setName("主線程");
		// 調用a對象的foo方法
		a.foo(b);
		System.out.println("進入了主線程之後");
	}

	public void run() {
		Thread.currentThread().setName("副線程");
		// 調用b對象的bar方法
		b.bar(a);
		System.out.println("進入了副線程之後");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();


		dl.init();
	}
}

值得注意的是:以上兩段代碼都是可能會發生死鎖,不是一定會發生,概率問題。即使正常運行也不一定就說明沒有死鎖。

8.補充的解決線程安全問題的方法(LOCK鎖)

上面介紹了兩種解決線程安全的問題,但JDK5.0之後新增了一種LOCK鎖的方式解決線程安全問題。代碼如下:

package com.atguigu.java1;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 解決線程安全問題的方式三:Lock鎖  --- JDK5.0新增
 *
 * 1. 面試題:synchronized 與 Lock的異同?
 *   相同:二者都可以解決線程安全問題
 *   不同:synchronized機制在執行完相應的同步代碼以後,自動的釋放同步監視器
 *        Lock需要手動的啓動同步(lock()),同時結束同步也需要手動的實現(unlock())
 *
 * 2.優先使用順序:
 * Lock → 同步代碼塊(已經進入了方法體,分配了相應資源) → 同步方法(在方法體之外)
 *
 *
 *  面試題:如何解決線程安全問題?有幾種方式
 * @author wudi
 * @create 2019-02-15 下午 3:38
 */
class Window implements Runnable{

    private int ticket = 100;
    //1.實例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.調用鎖定方法lock()(這裏的線程實現方式是實現Runable,當使用繼承Thread時應主要保證這個lock鎖對象是唯一的。即在上面定義鎖的時候加一個靜態)
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票號爲:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.調用解鎖方法:unlock()
                lock.unlock();
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

9.synchronized 與 Lock 的對比

  1.  Lock是顯式鎖(手動開啓和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
  2.  Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
  3. 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)

開發中建議的優先順序:
Lock → 同步代碼塊(已經進入了方法體,分配了相應資源) → 同步方法(在方法體之外)

10.JDK5.0後新增的兩種創建線程的方法   

  • 新增方式一:實現Callable 接口

與使用Runnable相比, Callable功能更強大些:

  1. 相比run()方法,可以有返回值
  2. 方法可以拋出異常(run中有異常只能用try-catch內部消化掉)
  3. 支持泛型的返回值
  4. 需要藉助FutureTask類,比如獲取返回結果
package com.atguigu.java2;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 創建線程的方式三:實現Callable接口。 --- JDK 5.0新增
 *
 *
 * 如何理解實現Callable接口的方式創建多線程比實現Runnable接口創建多線程方式強大?
 * 1. call()可以有返回值的。
 * 2. call()可以拋出異常,被外面的操作捕獲,獲取異常的信息
 * 3. Callable是支持泛型的
 *
 * @author wudi
 * @create 2019-02-15 下午 6:01
 */
//1.創建一個實現Callable的實現類
class NumThread implements Callable{
    //2.實現call方法,將此線程需要執行的操作聲明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class ThreadNew {
    public static void main(String[] args) {
        //3.創建Callable接口實現類的對象
        NumThread numThread = new NumThread();
        //4.將此Callable接口實現類的對象作爲傳遞到FutureTask構造器中,創建FutureTask的對象
        FutureTask futureTask = new FutureTask(numThread);
        //5.將FutureTask的對象作爲參數傳遞到Thread類的構造器中,創建Thread對象,並調用start()
        new Thread(futureTask).start();

        try {
            //6.獲取Callable中call方法的返回值
            //get()返回值即爲FutureTask構造器參數Callable實現類重寫的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("總和爲:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

關於FutureTask:

  • Future接口
  1. 可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
  2.  FutrueTask是Futrue接口的唯一的實現類
  3. FutureTask 同時實現了Runnable, Future接口。它既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值

這個返回值常用於一個線程需要等待另一個線程的返回值才能繼續執行下去。

  • 新增方式二:實現線程池
  1. 背景: 經常創建和銷燬、使用量特別大的資源,比如併發情況下的線程,對性能影響很大。
  2. 思路:提前 創建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷燬、實現重複利用。類似生活中的公共交通工具。
  3. 好處:

                   提高響應速度(減少了創建新線程的時間)

                   降低資源消耗(重複利用線程池中線程,不需要每次都創建)

                   便於線程管理:

                                            corePoolSize:核心池的大小
                                            maximumPoolSize:最大線程數
                                            keepAliveTime:線程沒有任務時最多保持多長時間後會終止

​​​​​​​線程池相關API

  • ​​​​​​​JDK 5.0起提供了線程池相關API:ExecutorService 和 Executors
  •  ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor
  1. void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行Runnable
  2.  <T> Future<T> submit(Callable<T> task):執行任務,有返回值,一般又來執行Callable
  3.  void shutdown() :關閉連接池
  •  Executors:工具類、線程池的工廠類,用於創建並返回不同類型的線程池
  1.  Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池
  2. Executors.newFixedThreadPool(n); 創建一個可重用固定線程數的線程池
  3.  Executors.newSingleThreadExecutor() :創建一個只有一個線程的線程池
  4.  Executors.newScheduledThreadPool(n):創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。

代碼演示

package com.atguigu.java2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 創建線程的方式四:使用線程池
 *
 * 好處:
 * 1.提高響應速度(減少了創建新線程的時間)
 * 2.降低資源消耗(重複利用線程池中線程,不需要每次都創建)
 * 3.便於線程管理
 *      corePoolSize:核心池的大小
 *      maximumPoolSize:最大線程數
 *      keepAliveTime:線程沒有任務時最多保持多長時間後會終止
 *
 *
 * 面試題:創建多線程有幾種方式?四種!
 * @author wudi
 * @create 2019-02-15 下午 6:30
 */

class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定線程數量的線程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //設置線程池的屬性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.執行指定的線程的操作。需要提供實現Runnable接口或Callable接口實現類的對象
        service.execute(new NumberThread());//適合適用於Runnable
        service.execute(new NumberThread1());//適合適用於Runnable

//        service.submit(Callable callable);//適合使用於Callable
        //3.關閉連接池
        service.shutdown();
    }

}

​​​​​​​                                         

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