1. 程式人生 > >Java多執行緒學習(基礎篇)

Java多執行緒學習(基礎篇)

1. java對執行緒的支援

java對執行緒的支援主要體現在Thread類以及Runable介面上,他們都位於java.lang包下,無論是Thread類還是Runable介面,它們都有public void run()方法,這個run方法為我們提供了執行緒實際工作時的程式碼,換句話說,我們的邏輯程式碼就可以寫在run方法體中。

那麼什麼時候該用Thread,什麼時候該用Runable呢
繼承Thread實現的模式是 定義多個執行緒,各自完成各自的任務. 實現Runnable實現的模式是 定義多個執行緒,實現一個任務.其實在實現一個任務用多個執行緒來做也可以用繼承Thread類來實現只是比較麻煩,一般我們用實現Runnable介面來實現,簡潔明瞭。大多數情況下,如果只想重寫 run() 方法,而不重寫其他 Thread 方法,那麼應使用 Runnable 介面。

2. 執行緒的常用方法
這裡寫圖片描述
3. 如何正確地停止執行緒
不要呼叫執行緒的stop()方法來停止執行緒,呼叫stop方法停止執行緒會有一種戛然而止的感覺,你並不知道它是執行到哪一步停止的,也不知道還有什麼邏輯沒執行完。應該設定一個結束標誌來結束執行緒。也不要呼叫interrupt()方法來停止執行緒,因為interrupt()方法的初衷並不是用於停止執行緒的,而是用於中斷執行緒。
4 interrupt
① interrupt的作用就是中斷執行緒,呼叫interput()方法會設定執行緒的中斷狀態。
② 然後呼叫執行緒的interrupted()【這是一個靜態方法】方法或isInterrupted()【這是一個例項方法】方法將會返回一個boolean值(執行緒的中斷狀態)。
③ 當一個執行緒,呼叫join,sleep方法而被阻塞時,會使執行緒的中斷狀態被清除,然後後面呼叫interrupted()或isInterrupted()方法都不能得到一個正確的結果,並且當前執行緒會收到一個InterruptedException異常,這也就是為什麼我們需要在程式碼sleep方法和join方法中進行try catch的原因。

    package com.glassbox.thread.demo;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 10:16
 * @todo
 */
public class InterruptTest extends Thread {
    public static void main(String[] args) {
        System.out.println("begin>>>>>>>>>>>>>>>");
        Thread interruptTest = new
InterruptTest(); interruptTest.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("interrupt Thread>>>>>>>"); interruptTest.interrupt(); System.out.println("stop>>>>>>>>>>>>>>>>"); } @Override public void run() { while (true) { System.out.println("Thread is running..."); /** * 這裡用這種方式代替sleep是因為呼叫sleep的時候會改變當前執行緒的“中斷狀態”,從而丟擲一個InterreptException異常 */ long time = System.currentTimeMillis(); while (System.currentTimeMillis() - time < 1000) { } } } }

輸出: 可以看到,當呼叫interrupt方法後,執行緒仍在輸出

begin>>>>>>>>>>>>>>>
Thread is running...
Thread is running...
Thread is running...
Thread is running...
interrupt Thread>>>>>>>
stop>>>>>>>>>>>>>>>>
Thread is running...
Thread is running...
Thread is running...
Thread is running...

可以把isInterrupted()的返回結果當做結束標誌

   @Override
    public void run() {
        while (!this.isInterrupted()) {
            System.out.println("Thread is running...");
            /**
             * 這裡用這種方式代替sleep是因為呼叫sleep的時候會改變當前執行緒的“中斷狀態”,從而丟擲一個InterreptException異常
             */
            long time = System.currentTimeMillis();
            while (System.currentTimeMillis() - time < 1000) {

            }
        }
    }

5. 執行緒中的能量不守恆
“爭用條件”:當多個執行緒同時共享訪問同一資料(記憶體區域)時,每個執行緒都嘗試操作該資料,從而導致資料被破壞(corrupted),這種現象稱為爭用條件。

package com.glassbox.thread.demo2;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 10:52
 * @todo
 */

/**
 * 宇宙的能量系統
 * 遵循能量守恆定律
 * 能量不會憑空產生也不會憑空消失,只會從一處轉移到另一處
 */
public class EnergySystem {

    //能量盒子,能儲存能量的地方
    private final double[] energyBox;

    /**
     *
     * @param n 能量盒子的數量
     * @param initalEnergy 每個能量盒子初始含有的能量值
     */
    public EnergySystem(int n, double initalEnergy) {
        this.energyBox = new double[n];
        for (int i = 0; i < energyBox.length; i++) {
            energyBox[i] = initalEnergy;

        }
    }

    /**
     * 能量的轉移,從一個盒子轉移到另外一個盒子
     * @param from 能量源
     * @param to 能量終點
     * @param amount 要轉移的能量值
     */
    public void transfer(int from, int to, double amount) {

        if(energyBox[from] < amount) {
            return;
        }
        System.out.println(Thread.currentThread().getName());
        energyBox[from] -= amount;
        System.out.printf("從%d轉移%10.2f單位能量到%d", from, amount, to);
        energyBox[to] += amount;
        System.out.printf("能量總和:%10.2f%n", getTotalEnergies());
    }

    /**
     * 獲取能量世界的能量總和
     * @return
     */
    private double getTotalEnergies() {
        double sum = 0;
        for (double amount : energyBox) {
            sum += amount;
        }
        return sum;
    }

    /**
     * 返回能量盒子的長度
     * @return
     */
    public int getBoxAmount() {
        return energyBox.length;
    }
}
package com.glassbox.thread.demo2;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 11:48
 * @todo
 */
public class EnergyTransferTask implements Runnable {

    //共享的能量世界
    private EnergySystem energySystem;
    //能量轉移的能源盒子下班
    private int fromBoxIndex;
    //單次能量轉移最大單元
    private double maxAmount;
    //最大休眠時間(毫秒)
    private int DELAY = 10;

    public EnergyTransferTask(EnergySystem energySystem, int fromBoxIndex, double max) {
        this.energySystem = energySystem;
        this.fromBoxIndex = fromBoxIndex;
        this.maxAmount = max;
    }

    @Override
    public void run() {
        try {
            while (true) {
                int toBox = (int) (energySystem.getBoxAmount() * Math.random());
                double amount = maxAmount * Math.random();
                energySystem.transfer(fromBoxIndex, toBox, amount);
                Thread.sleep((int) (DELAY * Math.random()));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
package com.glassbox.thread.demo2;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 11:59
 * @todo
 */
public class EnergySystemTest {
    //要構建的能量世界的盒子的數量
    public static final int BOX_AMOUNT = 100;
    //每個盒子的初始能量
    public static final double INITIAL_ENERGY = 1000;

    public static void main(String[] args) {
        EnergySystem energySystem = new EnergySystem(BOX_AMOUNT,INITIAL_ENERGY);
        for (int i = 0; i < BOX_AMOUNT; i++) {
            EnergyTransferTask task = new EnergyTransferTask(energySystem,i,INITIAL_ENERGY);
            Thread thread = new Thread(task,"TransferThread:" + i);
            thread.start();
        }
    }
}

輸出

38轉移    174.83單位能量到0能量總和: 100000.00
TransferThread:3838轉移     27.28單位能量到9能量總和: 100000.00
TransferThread:52
TransferThread:9898轉移    584.27單位能量到49能量總和:  99390.63
TransferThread:9898轉移    126.47單位能量到20能量總和:  99390.63
TransferThread:9898轉移    264.87單位能量到98能量總和:  99390.63
TransferThread:7474轉移    873.16單位能量到42能量總和:  99390.63
TransferThread:5050轉移    439.71單位能量到47能量總和:  99390.63
TransferThread:7070轉移     29.80單位能量到34能量總和:  99390.63
能量總和: 100000.00
TransferThread:7575轉移    377.40單位能量到78能量總和:  99390.63
TransferThread:9595轉移     74.24單位能量到76能量總和:  99390.63
能量總和: 100000.00
TransferThread:7171轉移    285.92單位能量到10能量總和:  99390.63
TransferThread:8383轉移    778.90單位能量到66能量總和:  99390.63
TransferThread:4646轉移    515.10單位能量到63能量總和:  99390.6352轉移    609.37單位能量到40能量總和: 100000.00

6. 那麼如何保持能量守恆呢
互斥與同步
互斥:字面意思是互相排斥,實際含義是,同一時間只能有一個執行緒對臨界區或關鍵資料進行操作(通過鎖)。
同步:執行緒間的一種通訊機制。一個執行緒它可以做一件事情,然後它可以用某種方式告訴別的執行緒它已經做完了(notify,wait,notifyAll)。

package com.glassbox.thread.demo2;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 10:52
 * @todo
 */

/**
 * 宇宙的能量系統
 * 遵循能量守恆定律
 * 能量不會憑空產生也不會憑空消失,只會從一處轉移到另一處
 */
public class EnergySystem {

    //能量盒子,能儲存能量的地方
    private final double[] energyBox;

    //新增一個鎖的物件
    private final Object lockObj = new Object();

    /**
     *
     * @param n 能量盒子的數量
     * @param initalEnergy 每個能量盒子初始含有的能量值
     */
    public EnergySystem(int n, double initalEnergy) {
        this.energyBox = new double[n];
        for (int i = 0; i < energyBox.length; i++) {
            energyBox[i] = initalEnergy;

        }
    }

    /**
     * 能量的轉移,從一個盒子轉移到另外一個盒子
     * @param from 能量源
     * @param to 能量終點
     * @param amount 要轉移的能量值
     */
    public void transfer(int from, int to, double amount) {

        /**
         * 通過對lockObj物件加鎖實現互斥
         */
        synchronized (lockObj) {
            //這裡當能量不足的時候就退出,但是在退出之後,這條執行緒仍然有機會去獲取cpu資源,從而再次要求加鎖,但是加鎖操作時有開銷的,這樣會降低系統的效能,所以當條件不滿足的時候我們可以讓這條執行緒等待,從而降低這條執行緒獲取鎖的開銷
            /*if(energyBox[from] < amount) {
                return;
            }*/

            /**
             * while迴圈,保證條件不滿足時任務都會被阻擋,而不是競爭cpu資源
             */
            while (energyBox[from] < amount) {
                try {
                    //當執行緒呼叫lockObj物件的wait()方法之後,當前的執行緒就會被置於lockObj物件的“等待集合wait set”中
                    lockObj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


            System.out.println(Thread.currentThread().getName());
            energyBox[from] -= amount;
            System.out.printf("從%d轉移%10.2f單位能量到%d", from, amount, to);
            energyBox[to] += amount;
            System.out.printf("能量總和:%10.2f%n", getTotalEnergies());
            //喚醒所有在lockObj物件上等待的執行緒
            lockObj.notifyAll();
        }

    }

    /**
     * 獲取能量世界的能量總和
     * @return
     */
    private double getTotalEnergies() {
        double sum = 0;
        for (double amount : energyBox) {
            sum += amount;
        }
        return sum;
    }

    /**
     * 返回能量盒子的長度
     * @return
     */
    public int getBoxAmount() {
        return energyBox.length;
    }
}

輸出

68轉移    765.10單位能量到80能量總和: 100000.00
TransferThread:6969轉移    564.22單位能量到33能量總和: 100000.00
TransferThread:6464轉移    941.32單位能量到96能量總和: 100000.00
TransferThread:9696轉移    511.90單位能量到74能量總和: 100000.00
TransferThread:44轉移    424.99單位能量到70能量總和: 100000.00
TransferThread:77轉移    429.73單位能量到33能量總和: 100000.00

7. 如何擴充套件java併發程式設計的知識
① 瞭解Java Memory Model
JMM描述了java執行緒如何通過記憶體進行互動的(通過這部分學習可以瞭解什麼是happens-before原則)
happens-before
synchronized,volatile&final(java是如何通過這幾個關鍵字來實現happens-before原則的)

② Locks物件和Condition物件(通過這兩個物件可以瞭解如何對程式進行加鎖以及同步的通訊)
java鎖機制和等待條件的高層實現
java.util.concurrent.locks

③ 執行緒安全性
原子性、可見性
java.util.concurrent.atomic(如何通過這個包來避免原子性程式設計的問題)
synchronized & volatile(當一個原子操作由多個語句構成時,又是如何通過synchronized實現原子性操作的)
DeadLocks

④ 多執行緒常用的互動模型(在java併發實現當中,有哪些類是實現了這些模型,可以供我們直接呼叫的)
Producer-Consumer模型
Read-Write Lock模型
Future模型
Woker Thread模型

⑤ Java5引入的併發程式設計工具
java.util.concurrent
執行緒池ExecutorService
Callable & Future
BlockingQueue

最後推薦兩邊書
這裡寫圖片描述

練習鞏固
題目:建立兩個執行緒,一個執行緒輸出1、3、5、…、99,另一個執行緒輸出2、4、6、…、100。最後的效果是兩個執行緒相互交替輸出,達到輸出1、2、3、4、…、99、100這樣順序輸出的效果。
以下是我自己的實現:

package com.glassbox.thread.test;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 16:08
 * @todo
 */
public class PrintNum implements Runnable {

    int i = 1;
    @Override
    public void run() {
        while (true) {
            //如果不是使用實現Runnable這種方式建立執行緒,這裡不要用this(自己建立一個鎖物件)
            synchronized (this) {
                //這裡先呼叫鎖物件的notify方法再呼叫wait方法是巧妙之處,大家細細體會,如果還不明白的話可以把notify方法放到wait方法後,然後執行輸出結果就一目瞭然了
                notify();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    i++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
package com.glassbox.thread.test;

import java.util.concurrent.*;

/**
 * @auther xiehuaxin
 * @create 2018-08-08 15:41
 * @todo
 */
public class ThreadExercise {
    /**
     * 任務佇列
     */
    private static ArrayBlockingQueue workQueue = new ArrayBlockingQueue(10);

    public static void main(String[] args) {

        /*
      執行緒池最大數量
     */
        int maximumPoolSizeSize = 100;
        /*
      執行緒活動保持時間
     */
        long keepAliveTime = 1;
        /*
      執行緒池的基本大小
     */
        int corePoolSize = 10;
        //建議手動建立執行緒池並且以執行緒池的形式建立執行緒
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSizeSize, keepAliveTime, TimeUnit.SECONDS, workQueue, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("hahaThread");
                return thread;
            }
        });
        PrintNum num = new PrintNum();
        executor.execute(num);
        executor.execute(num);
    }
}