Java高併發與多線程網絡編程


1. 線程介紹

概念

  • 程序:靜態的概念(資源分配的單位)
  • 進程:運行的程序(調度執行的單位)
  • 線程:一個程序中有多個事件同時執行,每個事件一個線程,多個線程共享代碼和數據空間

圖片源於網絡

2. 創建並啓動線程

當JVM啓動時,會創建一個非守護線程 main,作爲整個程序的入口,以及多個與系統相關的守護線程

package concurrency.chapter1;

public class TryConcurrency {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                write();
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                read();
            }
        }.start();
    }

    private static void write(){
        System.out.println("start to write");
        try {
            Thread.sleep(1000*10L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("write success!");
    }

    private static void read(){
        System.out.println("start to read");
        try {
            Thread.sleep(1000*10L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("read success!");
    }
}

線程生命週期(Thread.State)
圖片源自網絡

除了新生狀態和死亡狀態,任意狀態都可能發生線程死亡。

  • NEW:新生狀態,作爲普通java對象,尚未啓動
  • RUNNABLE:在JVM中執行(包含就緒狀態和運行狀態)
  • BLOCKED:被阻塞
  • WAITING:正在等待另一個線程執行特定動作
  • TIMED_WAITING:正在等待另一個線程達到指定運行時間
  • TERMINATED:死亡狀態

傳統的多線程共享數據方法是使用static修飾,但是由於static修飾的數據將在整個程序結束之後才釋放,因此比較耗資源。可參考內存模型,類加載
而且這種方式會發生併發問題。

實例:銀行有多個櫃檯,櫃檯共用一個叫號系統

package concurrency.chapter2;

/**
 * 叫號的櫃檯
 */
public class TicketWindow extends Thread{
    private static final int MAX=500;
    private static int index=1;
    private String name;

    TicketWindow(String name){
        this.name=name;
    }
    @Override
    public void run() {
        while (index<=MAX){
            System.out.println(name+" 叫號:"+(index++));
        }
    }
}
package concurrency.chapter2;

/**
 * 銀行叫號
 */
public class Bank {
    public static void main(String[] args) {
        final TicketWindow t1 = new TicketWindow("一號櫃檯");
        final TicketWindow t2 = new TicketWindow("二號櫃檯");
        final TicketWindow t3 = new TicketWindow("三號櫃檯");
        t1.start();
        t2.start();
        t3.start();
    }
}

在這裏插入圖片描述

上述方式通過繼承Thread的形式實現多線程,但是顯然,業務數據與被混在了線程類當中,這種方式顯得混亂,我們使用更好的辦法——使用runnable。

package concurrency.chapter2;

public class Bank2 {
    public static void main(String[] args) {
        final TicketWindow2 ticketWindow2 = new TicketWindow2();
        Thread t1 = new Thread(ticketWindow2,"1號櫃檯");
        Thread t2 = new Thread(ticketWindow2,"2號櫃檯");
        Thread t3 = new Thread(ticketWindow2,"3號櫃檯");
        t1.start();
        t2.start();
        t3.start();

    }
}

package concurrency.chapter2;

/**
 * 叫號的櫃檯
 */
public class TicketWindow2 implements Runnable{
    private static final int MAX=500;
    private static int index=1;

    public void run() {
        while (index<=MAX){
            System.out.println(Thread.currentThread().getName()+" 叫號:"+(index++));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

依舊有線程安全問題。
在這裏插入圖片描述

3. 函數式接口編程

Thread支持函數式接口編程,Runnable接口就是一個函數式接口。

首先看看什麼是函數式接口編程。

Java中使用@FunctionalInterface註解標註函數式接口。

所謂的函數式接口,當然首先是一個接口,然後就是在這個接口裏面只能有一個抽象方法

這種類型的接口也稱爲SAM接口,即Single Abstract Method interfaces
特點

  • 接口有且僅有一個抽象方法
  • 允許定義靜態方法
  • 允許定義默認方法
  • 允許java.lang.Object中的public方法

@FunctionalInterface註解不是必須的,如果一個接口符合"函數式接口"定義,那麼加不加該註解都沒有影響。加上該註解能夠更好地讓編譯器進行檢查。如果編寫的不是函數式接口,但是加上了@FunctionInterface,那麼編譯器會報錯

例子

// 正確的函數式接口
@FunctionalInterface
public interface TestInterface {
 
    // 抽象方法
    public void sub();
 
    // java.lang.Object中的public方法
    public boolean equals(Object var1);
 
    // 默認方法
    public default void defaultMethod(){
    
    }
 
    // 靜態方法
    public static void staticMethod(){
 
    }
}

// 錯誤的函數式接口(有多個抽象方法)
@FunctionalInterface
public interface TestInterface2 {

    void add();
    
    void sub();
}

函數式接口其實策略模式中的“策略”

計算納稅款的實例:
納稅款的計算方式可能多變,因此將計算方式抽象出來作爲一個接口(策略),然後可以實現動態的改變改計算方法。

接口:傳入工資

package concurrency.func;

@FunctionalInterface
public interface SimpleCalculator {
    double calculated(double money);
}

計算器:關聯上述接口

package concurrency.func;

public class Calculator {
    private double money;
    private final SimpleCalculator calculator;
    Calculator(double money,SimpleCalculator calculator){
        this.calculator=calculator;
        this.money=money;
    }
    public double calculated(){
        return calculator.calculated(money);
    }
}

客戶端

public class CalculatorMain {
    public static void main(String[] args) {
        final Calculator calculator = new Calculator(10000, (m)->m*0.2 );
        System.out.println(calculator.calculated());
    }
}

通過傳入不同的lambda表達式,實現不同的計算方法。

通過函數式接口編程,將銀行叫號整合到一個類中。

public class Bank3 {
    public final static int MAX=50;
    public static int index=1;
    public static void main(String[] args) {
        final Runnable runnable = ()->{
            while (index<=50){
                System.out.println(Thread.currentThread().getName()+" 櫃檯叫號 "+(index++));
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable,"1號櫃檯").start();
        new Thread(runnable,"2號櫃檯").start();
        new Thread(runnable,"3號櫃檯").start();
    }
}

上述代碼同樣有嚴重的併發問題。

4. Thread 構造器

構造器
1.Thread()
只做命名工作,命名方式爲
Thread-i (i從0開始)

2.Thread(Runnable)
因爲runnable只有一個run方法,因此相當於聲明一個run方法。如果Runnable爲空,則線程什麼也不做。

3.Thread(String)
傳入線程名字,由於Runnable爲空,因此什麼也不做。

4.Thread(Runnable,String)
聲明run方法,以及線程名字,此時就是一個可工作的線程。

5.ThreadGroup的概念:
在構造器中可以傳入一個ThreadGroup,當ThreadGroup爲空時,查看源碼後可知會設定CurrentThread的ThreadGroup爲當前的ThreadGroup。
CurrentThread就是啓動當前線程的線程。
比如在main函數中啓動的線程,那麼CurrentThread就是main,main的ThreadGroup名爲main

6.stacksize概念
首先得知道基本的內存模型
在這裏插入圖片描述
堆和方法區是線程共享,而虛擬機棧是線程私有,stacksize主要影響虛擬機棧。
棧作爲內存結構,肯定有限制大小,通過改變stacksize,能夠更改自定義線程的虛擬機棧大小。
在官方文檔中有說明,stacksize高度依賴平臺,不同的運行平臺,stacksize可能不起作用。
如果不傳stacksize,則默認爲0,表示會被忽略。
stacksize被JVM使用,Java中沒有直接引用。

5. 守護線程

Daemon:守護線程,會跟隨父線程結束。
非守護線程(默認),父線程結束之後,依舊運行。

這麼理解: “守護”的對象指的是系統,而非線程本身。當父線程結束之後子線程依舊還在跑,可能出現一些意外的情況,因此需要對系統進行守護。

問題:
1.非守護線程outer中有個長耗時的守護線程inner,那麼當outer結束時,inner是什麼狀態?
inner是守護線程,會隨父線程結束,因此當outer結束後,inner未執行完就結束了。

2.守護線程outer中有個長耗時的非守護線程inner,那麼當outer結束時,inner是什麼狀態?
inner是非守護線程,不會隨父線程結束,因此當outer結束後它會繼續運行。

無論外層的線程是什麼類型,只關注本身的類型即可。

通過T.setDaemon(true)設爲守護線程。
注意,只有在start之前設置才生效

線程關係

以下內容爲個人實驗結果,可能存在偏頗之處

線程之間的關係分爲三種:
最外層:線程之間是平等的,線程開始之後,就有了自己獨立的運行空間,不會受執行該線程的線程所影響
舉例:守護線程中執行非守護線程,當守護線程隨着main結束之後,內部的非守護線程依舊在執行,既不會阻塞守護線程,也不會跟着守護線程結束。

package concurrency.chapter1;

public class Try {
    public static void main(String[] args) throws InterruptedException {
        // 守護線程中套一個長時間的非守護線程,
        // 一般來說,當main結束後,守護線程會結束,但是由於其內還有個非守護線程,那麼它會被阻塞住嗎?
        Thread t1 = new Thread(()->{
            Thread t2 = new Thread(()->{
                while (true){
                    System.out.println("儘管父線程都結束了,但是我還在跑");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t2.setDaemon(false);
            t2.start();
            System.out.println("t1結束");
        });
        t1.setDaemon(true);
        t1.start();
        // 模擬t1在工作,
        // 如果不加主線程睡眠,那麼程序會直接結束
        // 猜測:t2的創建需要耗時間,還沒等t2創建,main就結束了,也導致守護線程t1結束
        Thread.sleep(100);
    }
}

關係二:線程之間存在執行與被執行關係
舉例:CurrentThread、Thread的靜態方法受執行位置所影響、join等都是這個關係的體現。
比較重要的應用就是內部爲守護線程時,內部線程的運行時間受外部線程所影響。

關係三:ThreadGroup
這個通常是人爲設置,將多個線程放在一個組中進行管理。

6. join

讓CurrentThread等待join線程執行完畢,CurrentThread才繼續前進。

在main方法中使用Thread.CurrentThread.join(),相當於讓main等待自己結束,這會陷入死循環。

7. interrupt

當線程處於block狀態(wait、join、sleep)時,調用interrupt會讓線程接收一個InterruptException(如果線程中沒有捕獲該異常,即便interrupt了,線程也不會收到中斷信號)

interrupt不會中斷線程,而是將線程的狀態改爲中斷。通過在線程內捕獲該狀態,然後邊寫處理代碼,實現中斷。

block狀態也有對象之分:x.wait、x.sleep,當x是誰(或者在哪個線程內)調用,則wait、sleep對象就是x(或當前調用的線程),但是x.join,對象僅僅指CurrentThread,如在main中調用thread1.join(),join的對象是main,即main進入join狀態,而非thread1。
根據上述描述,interrupt可以打斷block狀態的線程,當線程處於join時,可以進行打斷。
但是注意進入join狀態的線程是哪個。

可以試試以下實驗:

package concurrency.chapter3;

public class Interrupt {
    public static void main(String[] args) {
        // 啓動測試線程對象 t
        Thread t = new Thread(()->{
           while (true){
           }
        });
        t.start();
        // 由於t.join()之後的代碼都不執行,因此新建一個臨時線程用於監控t.join()之後的狀態
        new Thread(()->{
            try {
                // sleep保證t.join()執行完畢
                Thread.sleep(1000);
                // 拿到main線程
                ThreadGroup group = Thread.currentThread().getThreadGroup();
                Thread[] threads = new Thread[group.activeCount()];
                group.enumerate(threads);
                for (Thread thread:threads){
                    if (thread.getName().equals("main")){
                        System.out.println("main state:"+thread.getState());
                        System.out.println("t state:"+ t.getState());
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        try {
            // 在main中調用t.join()
            System.out.println("t.join() invoked in main()");
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

結果爲

t.join() invoked in main()
main state:WAITING
t state:RUNNABLE

說明t.join()之後,main進入join阻塞狀態,而非t。

因此,我們通過以下代碼想實現通過join打斷t是不行的,因此進入join的不是t,而是CurrentThread

package concurrency.chapter3;

public class Interrupt {
    public static void main(String[] args) {
        // 啓動測試線程對象 t
        Thread t = new Thread(()->{
           while (true){
           }
        });
        t.start();
        // 由於t.join()之後的代碼都不執行,因此新建一個臨時線程用於監控t.join()之後的狀態
        new Thread(()->{
            System.out.println("在臨時線程中interrupt->t");
            t.interrupt();
        }).start();
        try {
            // 在main中調用t.join()
            System.out.println("t.join() invoked in main()");
            t.join();
        } catch (InterruptedException e) {
            System.out.println("t在join過程中被interrupt");
        }
    }
}

以上代碼相當於改變了t的interrupt狀態爲中斷狀態,但是沒有對象去捕獲該異常進行處理。
因此,通過join捕獲interrupt異常,只能捕獲CurrentThread。

8. 優雅的結束線程

方式一:flag

package concurrency.chapter4;

public class Stop1 {
    public static class Test extends Thread{
        private volatile boolean start=true;

        @Override
        public void run() {
            while (start){
            }
        }
        public void shutdown(){
            this.start=false;
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.start();
        // 模擬test工作耗時
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        test.shutdown();
    }
}

方式二:使用interrupt+block

package concurrency.chapter4;

public class Stop2 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while (true) {
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    System.out.println("結束進程");
                    return;
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

方式三:Thread.interrupted()
Thread.interrupted()用於檢測當前線程是否被interrupt,相當於thread.isInterrupted(),只是一個是靜態方法,一個是實例方法。
之所以多一個靜態方法,是因爲在lambda或者匿名內部類中不能使用實例方法。

package concurrency.chapter4;

public class Stop2 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while (true) {
                if (Thread.interrupted()){
                    System.out.println("結束進程");
                    return;
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

考慮以下情況,在線程中,有個事件本身就block了(比如預計10秒,但是一個小時也沒結束),這時候,它無法讀取到flag,也監聽不到interrupted,該如何結束它?
利用守護線程與非守護線程的概念。
父線程結束,守護線程也會結束,因此,我們可以用一個父線程來執行目標線程,並將目標線程設爲守護線程。通過控制父線程的結束,實現目標線程的結束。

方式四:利用守護線程
**父線程ThreadService **

package concurrency.chapter4;

public class ThreadService {
    private Thread executeThread;
    private volatile boolean finished=false;
    private String taskName;
    ThreadService(String taskName){
        this.taskName=taskName;
    }
    // 將執行任務設爲守護線程
    public void execute(Runnable task){
        executeThread = new Thread(()->{
            Thread runner = new Thread(task);
            runner.setDaemon(true);
            runner.start();
            // executeThread等待runner執行完畢
            try {
                runner.join();
                finished=true;
            } catch (InterruptedException e) {
                // 打斷,結束程序體
            }
        });
        executeThread.start();
    }
    public void shutdown(long mills){
        long currentTime = System.currentTimeMillis();
        // 如果沒有結束
        while (!finished){
            // 如果超時
            if (System.currentTimeMillis()-currentTime>mills){
                // 並非interrupt()結束了線程,而是interrupt()控制了線程執行語句
                // 調用本行代碼時,會進入第21行,
                // 由於executeThread本身沒有其他處理邏輯,因此executeThread結束,同時runner也跟着結束
                executeThread.interrupt();
                System.out.println(this.taskName+" 任務超時!強行結束");
                break;
            }
            // 由於finished加了volatile關鍵字,因此當線程結束時,此處的finished也會被觀測爲true
            // 所以如果沒有超時,while也能夠自動斷開
        }
        finished=false;
    }
}

客戶端實例

package concurrency.chapter4;

public class Stop3 {
    public static void main(String[] args) {
        ThreadService threadService = new ThreadService("test線程");
        long start = System.currentTimeMillis();
        threadService.execute(()->{
            // 執行一個非常重的任務
//            while (true){
//            }
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadService.shutdown(1000);
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }
}

9. 線程安全、數據共享

同步鎖 synchronized
可鎖對象:

  1. 實例對象:this,synchronized作爲實例方法關鍵字時,
  2. 類對象:class,synchronized作用於靜態方法、修飾靜態對象時
  3. 塊對象:鎖住對應字節碼

死鎖:相互等待對方的資源,一般發生在鎖套鎖的情況。

synchronized核心

實例鎖:鎖特定實例,this
類鎖:鎖類,class

每次執行之前模擬以下過程:判斷是否需要鎖-->鎖競爭-->得到鎖-->執行-->釋放鎖

①synchronized修飾非靜態方法-->鎖this
	-->1.1 多個線程訪問同一個對象的該方法-->同步
	-->1.2 同一個對象,一個線程訪問synchronized方法,另一個對象訪問非synchronized方法-->異步
	-->1.3 同一個對象,一個線程訪問synchronized方法,另一個對象訪問另一個synchronized方法-->同步
結論:一個實例只有一個this鎖,
對於1.1,存在鎖競爭的過程,因此同步
對於1.2,非synchronized方法不需要鎖競爭,因此異步
對於1.3,儘管是兩個不同的synchronized方法,但是是同一個鎖,也需要鎖競爭,因此同步
	
②synchronized修飾靜態方法-->鎖class
	-->2.1 一個線程訪問synchronized靜態方法,另一個對象訪問非synchronized靜態方法-->異步
    -->2.2 一個線程訪問synchronized靜態方法,另一個對象訪問另一個synchronized靜態方法-->同步
對於2.1,非synchronized方法不需要鎖競爭,因此異步
對於2.2,儘管是兩個不同的synchronized方法,但是是同一個鎖,也需要鎖競爭,因此同步


③定義靜態變量lock,通過synchronized(lock){}對代碼塊進行加鎖-->鎖lock變量
	-->3.1 一個線程訪問synchronized靜態方法,另一個線程訪問包含synchronized(lock){}的靜態方法-->異步
	-->3.2 一個線程訪問synchronized靜態方法,另一個線程訪問包含synchronized(當前類名.class){}的靜態方法-->同步
對於3.1,一個鎖是class,一個鎖是lock,不存在鎖競爭,因此異步
對於3.2,兩個鎖都是當前類名.class,存在鎖競爭,因此異步
注意,有種lock的寫法是 private static 當前類名 lock = new 當前類名();
此時的lock與當前類名.class依舊不是同一個鎖。

④定義靜態變量int i,一個線程運行synchronized方法修改i,另一個線程運行費synchronized方法修改i,此時是異步,而且數據不安全。
--> synchronized沒有鎖住資源,只鎖住了代碼,在其他入口訪問同一份資源依舊會出現數據不同步問題。

⑤定義靜態代碼塊內的synchronized
static{
	synchronized(xx){}
}
-->靜態代碼塊會阻塞該類中的所有資源,因爲加載靜態代碼塊屬於類的初始化過程

綜上,其實synchronized的核心就在於加鎖過程,我們需要判斷當前鎖是否存在、是否是同一個鎖對象,進而判斷是否存在鎖競爭,從而得知是否是同步、異步。

上述內容皆爲實驗所得,如有遺漏,敬請留言。

10. 死鎖

死鎖:相互等待對方的資源,一般發生在鎖套鎖的情況。
A等待B,B等待C,C等待A

常見於以下情況:
在使用第三方service時,第三方service需要傳入我們自己寫的類。我們自己寫的類又加了鎖,就可能出現鎖套鎖的情況。

檢測死鎖的方法:
打開cmd
jps 查看所有java進程
jstack 進程號 如果有死鎖,控制檯會通知

11. 線程間的通訊(生產者與消費者)

模型一:單生產者+單消費者

package concurrency.chapter6;

// 生產者消費者模型
public class ProductorConsumerModel {
    private int i;
    private boolean needProduct=true;

    void product(){
        synchronized (this) {
            // 如果需要生產,那麼就生產
            if (needProduct) {
                System.out.println("生產:" + (i++));
                // 生產完了通知消費者消費
                this.notify();
                needProduct=false;
            }else{
                // 如果不需要生產,那麼就讓生產者等待。
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    void comsumed(){
        synchronized (this) {
            // 如果不需要生產,那就消費
            if (!needProduct) {
                System.out.println("消費:" + (--i));
                // 消費完了通知生產者生產
                this.notify();
                needProduct=true;
            }else {
                // 需要生產,就讓消費者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        ProductorConsumerModel model = new ProductorConsumerModel();
        new Thread(()->{
            while (true){
                model.product();
            }
        }).start();
        new Thread(()->{
            while (true) {
                model.comsumed();
            }
        }).start();
    }
}

注意,xx.wait()是讓執行這個方法的線程等待,直到被通知。xx.notify()是喚醒被當前鎖鎖住的對象

這個模型只允許構建一個生產者線程和一個消費者線程,多個生產者消費者的時候會出現假死鎖狀態。
因爲不能確定notify作用於哪個對象,導致所有線程進入wait狀態

模型二:多消費者-多生產者
把模型一中的notify改爲notifyAll()即可(注意被喚醒並且被執行的線程是從上次阻塞的位置從下開始運行,也就是從wait()方法後開始執行。
因此判斷是否進入某一線程的條件 是用while判斷,而不是用If判斷判斷。

package concurrency.chapter6;

// 生產者消費者模型
public class ProductorConsumerModel2 {
    private int i;
    private boolean needProduct=true;

    void product(){
        synchronized (this) {
            // 如果需要生產,那麼就生產
            while (needProduct) {
                System.out.println(Thread.currentThread().getName() + "生產:" + (i++));
                // 生產完了通知消費者消費
                this.notifyAll();
                needProduct = false;
            }
            // 如果不需要生產,那麼就讓生產者等待。
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    void comsumed(){
        synchronized (this) {
            // 如果不需要生產,那就消費
            while (!needProduct) {
                System.out.println(Thread.currentThread().getName()+"消費:" + (--i));
                // 消費完了通知生產者生產
                this.notifyAll();
                needProduct=true;
            }
            // 需要生產,就讓消費者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    public static void main(String[] args) {
        ProductorConsumerModel2 model = new ProductorConsumerModel2();
        new Thread(()->{
            while (true){
                model.product();
            }
        },"P1").start();

        new Thread(()->{
            while (true){
                model.product();
            }
        },"P2").start();
        new Thread(()->{
            while (true){
                model.product();
            }
        },"P3").start();
        new Thread(()->{
            while (true){
                model.product();
            }
        },"P4").start();

        new Thread(()->{
            while (true) {
                model.comsumed();
            }
        },"C1").start();
        new Thread(()->{
            while (true) {
                model.comsumed();
            }
        },"C2").start();
    }
}

方式三:將數據、生產者、消費者解耦(重點掌握)

package concurrency.chapter6;

import concurrency.chapter4.ThreadService;

import java.util.stream.Stream;

// 生產者消費者模型
public class ProductorConsumerModel4 {

    public static void main(String[] args) {
        Data data = new Data();
        Stream.of("P1","P2").forEach(name->
                new Thread(()->{
                    Productor productor = new Productor(data);
                    while (true){
                        productor.producted();
                    }
                },name).start());

        Stream.of("C1","C2").forEach(name->
                new Thread(()->{
                    Consumer consumer = new Consumer(data);
                    while (true){
                        consumer.consumed();
                    }
                },name).start());
    }
}

// 數據容器
class Data{
    private int i=0;
    private boolean needPush;

    synchronized public void push() {
        while (needPush){
            System.out.println(Thread.currentThread().getName()+"生產: "+ (++i));
            needPush=!needPush;
            notifyAll();
        }
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public void pop() {
        while (!needPush){
            System.out.println(Thread.currentThread().getName()+"消費: "+ (i--));
            needPush=!needPush;
            notifyAll();
        }
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 生產者
class Productor{
    private Data data;
    Productor(Data data){
        this.data=data;
    }
    public void producted(){
        data.push();
    }
}

// 消費者
class Consumer{
    private Data data;
    Consumer(Data data){
        this.data=data;
    }
    public void consumed(){
        data.pop();
    }

說明:原理其實非常簡單:生產者和消費者的速度都是一樣的,那麼理論情況下,生產一個,就被消費一個。因此,需要同步的對象就是當前這個被生產/被消費的數據,與之相關的操作就是“生產”與“消費”,因此只需要讓這兩個操作去競爭同一個鎖就行了

思考:如果是一個生產者和一個消費者,並且生產者和消費者都在搶一個鎖(對數據的寫入和寫出權),這不還是單線程麼?
此外,如果隊列入口一個鎖,出口一個鎖,生產者和消費者各搶各的鎖,用容器上限來判斷是否生產\消費,那麼多個生產者和消費者的速度是不是還是跟單個生產者\消費者一樣?
畢竟隊列只有一個入口和出口,同一時刻只允許一個生產者和消費者操作。
上述問題博主暫時也沒找到答案,等以後學會了回來補充,如果各位看官懂,敬請留言指點

12. sleep與wait的區別

1.sleep是Thread方法,wait是Object方法
2.sleep不會釋放鎖,wait會釋放鎖
3.sleep不依賴於鎖,wait的使用依賴於鎖
4.sleep不需要喚醒,wait需要(wait(time)除外)

13. 綜合案例–數據採集

線程切換是需要開銷的,多線程效率是一個開口向下的拋物線,當線程過多的時候,效率會越來越慢。
案例:對n臺機器進行數據採集工作,顯然,我們需要定義一定數量的線程,當某個線程結束後,再啓動一個線程去採集,保證線程的數量不超過設定的最大值。

假設有10臺機器,線程最大數爲5。

package concurrency.chapter7;

import java.util.*;
import java.util.stream.Stream;

public class DataCapture {
    final private static int MAXSIZE=5;
    // FIFO隊列,用於控制運行時的線程個數
    final static private LinkedList<Object> CONTROLS = new LinkedList<>();

    public static void main(String[] args) {
        // 由於流是一次性的,我們用一個容器臨時保存線程,保證在後面能夠讓所有線程join
        List<Thread> threads = new ArrayList<>();
        // 創建10個線程,注意我們能夠同時運行的線程最大個數爲5,因此通過wait對線程的運行進行控制
        Stream.of("M1","M2","M3","M4","M5","M6","M7","M8","M9","M10")
                .map(DataCapture::threadCreate)
                .forEach(t->{
                    t.start();
                    threads.add(t);
                });
        // 拿到所有線程,進行join
        threads.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Optional.of("所有線程工作結束").ifPresent(System.out::println);
    }
    private static Thread threadCreate(String name){
        return new Thread(()->{
            // 運行時控制線程個數,進隊列搶鎖
            synchronized (CONTROLS) {
                // 如果當前個數大於MAXSIZE了(也就是第六個)就讓其wait
                while (CONTROLS.size() >= MAXSIZE) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ": 正在等待!");
                        CONTROLS.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                CONTROLS.addLast(new Object());
                // 這句通知由隊列發出(即在synchronized中),能夠清楚看到工作順序
                System.out.println(Thread.currentThread().getName()+": 開始工作");
            }

            // 工作時都在自己的工作空間,不需要進行synchronized
            // 模擬工作耗時
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": 工作結束");
            // 出隊列搶鎖
            synchronized (CONTROLS){
                // 工作完畢,自己退出隊列,並通知其他線程
                CONTROLS.removeFirst();
                CONTROLS.notifyAll();
            }
        },name);
    }
}

在個數判斷中必須使用while (CONTROLS.size() >= MAXSIZE) {,大於等於號,才能保證同時運行5個線程,我也不知道爲啥。按照邏輯來說應該是用大於號,望懂得朋友留言告知。

14. 顯式鎖(實現自定義鎖)

API接口設計

package concurrency.chapter8;

import java.util.Collection;

// 顯式鎖API接口
public interface Lock {
    // 超時異常
    class TimeOutException extends Exception{
        TimeOutException(String msg){
            super(msg);
        }
    }
    // 加鎖
    void lock() throws InterruptedException;
    // 按時加鎖
    void lock(long mills) throws InterruptedException,TimeOutException;
    // 解鎖
    void unlock();
    // 查看阻塞線程
    Collection<Thread> getBlockedThreads();
    // 查看阻塞個數
    int getBlockedSize();
}

實現

package concurrency.chapter8;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

// 通過boolean值去操控鎖
package concurrency.chapter8;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeoutException;

// 通過boolean值去操控鎖
public class BooleanLock implements Lock {
    // 如果LOCK爲true,說明鎖被人持有,否則說明鎖爲空閒狀態
    private boolean LOCK;

    // 保證當前操作鎖的對象式currentThread,不然其他線程也能自由調用lock和unlock
    private Thread currentThread;

    // 保存當前被阻塞的線程
    private Collection<Thread> blockedThreads = new ArrayList<>();

    // synchronized不能在接口中聲明,在這裏進行標註
    @Override
    synchronized public void lock() throws InterruptedException {
        // 如果鎖被人持有,那就等待
        while (LOCK){
            this.wait();
            this.blockedThreads.add(Thread.currentThread());
        }
        // 結束while之後說明鎖被當前線程拿到,那就更改鎖狀態
        this.blockedThreads.remove(Thread.currentThread());
        LOCK=true;
        this.currentThread = Thread.currentThread();
    }

    // synchronized加的鎖不具備超時的能力,因此我們自定義超時鎖
    @Override
    synchronized public void lock(long mills) throws InterruptedException, TimeOutException {
        if (mills<=0){
            lock();
            return;
        }
        // flag用於判斷是否超時
        long flag = mills;
        // timeout表示超時時間
        long timeout = System.currentTimeMillis()+mills;
        // 如果鎖被持有,就讓其等待
        while (LOCK){
            if (flag<=0){
                throw new TimeOutException(Thread.currentThread().getName()+"Time out");
            }
            blockedThreads.add(Thread.currentThread());
            this.wait();
            flag = timeout-System.currentTimeMillis();
        }
        // 拿到鎖
        this.LOCK=true;
        blockedThreads.remove(Thread.currentThread());
        this.currentThread = Thread.currentThread();
    }

    @Override
    synchronized public void unlock() {
        // 如果鎖被人持有,並且當前試圖解鎖的也是當前線程,那就釋放鎖
        while (LOCK && this.currentThread==Thread.currentThread()){
            System.out.println("鎖已被釋放");
            this.notifyAll();
            LOCK=false;
        }
        // 如果鎖處於釋放狀態,那就不做操作
    }

    @Override
    public Collection<Thread> getBlockedThreads() {
        // unmodifiableCollection 在返回的過程中,不允許對其進行修改
        return Collections.unmodifiableCollection(blockedThreads);
    }

    @Override
    public int getBlockedSize() {
        return blockedThreads.size();
    }
}

客戶端測試

package concurrency.chapter8;

import com.sun.org.apache.xpath.internal.operations.Bool;

import java.util.stream.Stream;

public class LockTest {
    public static void main(String[] args) {
        Lock lock = new BooleanLock();
        // 普通lock測試
        Stream.of("T1","T2","T3")
                .forEach(name->{
                    new Thread(()->{
                        // 當前線程拿到鎖,開始工作
                        try {
                            lock.lock();
                            System.out.println(Thread.currentThread().getName()+"  拿到鎖");
                            work();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }finally {
                            //結束後釋放鎖
                            lock.unlock();
                        }
                    },name).start();
                });
        // main函數視圖釋放鎖,但是我們有驗證,實際操作不了
        lock.unlock();

        // 超時lock測試
        Stream.of("T4","T5","T6")
                .forEach(name->{
                    new Thread(()->{
                        // 當前線程拿到鎖,開始工作
                        try {
                            lock.lock(100L);
                            System.out.println(Thread.currentThread().getName()+"  拿到鎖");
                            work();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (Lock.TimeOutException e) {
                            System.out.println(Thread.currentThread().getName()+" 超時!");
                        } finally {
                            //結束後釋放鎖
                            lock.unlock();
                        }
                    },name).start();
                });
        // main函數視圖釋放鎖,但是我們有驗證,實際操作不了
        lock.unlock();
    }
    // 模擬工作
    private static void work(){
        try {
            System.out.println(Thread.currentThread().getName()+"  正在工作");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

15. 鉤子方法處理系統退出工作

當系統被終止的時候,往往還有很多連接資源沒有關閉,比如數據庫連接、網絡連接等等,因此我們在終止程序的時候,需要一些關閉各種資源的操作——用鉤子方法。

package concurrency.chapter9;

public class HookToExit {
    public static void main(String[] args) {
        // 鉤子方法
        Runtime.getRuntime().addShutdownHook(new Thread(()->{
            System.out.println("系統正在退出");
            //進行退出資源回收
            nofigyAndRelease();
        }));

        System.out.println("開始工作");
        while (true){

        }
    }
    private static void nofigyAndRelease() {
        //這裏處理資源回收或通知
        System.out.println("關閉緩存");
        System.out.println("關閉數據源連接");
        System.out.println("關閉socket");
        System.out.println("系統已退出");
    }
}

也能在nofigyAndRelease可以捕獲異常

上述代碼在linux下測試可以看到清楚的效果。
無論是手動shutdowm還是kill進程,都能處理退出工作。
注意kill -9 是強制終結命令,無法完成退出處理工作。

16. ThreadException與stackTrace(瞭解)

package concurrency.chapter9;

import java.util.Optional;
import java.util.stream.Stream;

public class ThreadException {
    private static int A=1;
    private static int B=0;
    public static void main(String[] args){
        Thread t = new Thread(()->{
            // 這個異常無法拋出,只能try-catch
            try {
                Thread.sleep(100);
                A/=B;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        // 這裏捕獲異常並處理
        t.setUncaughtExceptionHandler((group,e)->{
            System.out.println(group);
            System.out.println(e);
        });
        System.out.println("打印方法調用棧");
        Stream.of(Thread.currentThread().getStackTrace())
                .filter(e->!e.isNativeMethod())
                .forEach(e-> Optional.of("類名:"+e.getClassName()+"  方法名:"+e.getMethodName()+"  行數:"+e.getLineNumber()).ifPresent(System.out::println));
    }

}

17. ThreadGroup

如果不設置group,會將線程加到當前group中。
ThreadGroup是樹形結構。
不能跨ThreadGroup訪問(既不能訪問父group,也不能訪問兄弟group)

group.destroy(),銷燬線程組,如果期內還有活躍的線程,拋出異常。

group.enumerate(Thread[] list【, boolean recurse】);
拷貝線程到list中,recurse指定是否需要對其內的其他線程組進行遞歸拷貝(深拷貝)
不加recurse則默認爲true

group.interrupt() 打斷其內的所有線程(遞歸式)

group.setDaemon()
線程組的Daemon跟線程不一樣,線程組的Daemon指的是,如果線程組被destroy或其內的所有線程都執行結束,那就銷燬該線程。
也就是說,非Daemon線程組需要手動回收
如果創建ThreadGroup時制定了parentThreadGroup,則Daemon與parent一致。

18. 手寫線程池

概念:
1.任務隊列:所有執行線程去任務隊列裏面拿任務執行
2.線程隊列:線程池有一個線程隊列
3.拒絕策略(拋出異常、丟棄、阻塞、臨時隊列):當任務數量超過線程池設定的可接受數量時,進行拒絕的處理策略
4.容量:線程池的線程隊列個數可以動態變化。

package chapter10;

import java.util.*;

// 線程池的簡單實現
public class ThreadPool extends Thread {

    //定義線程池大小
    private int size;

    //定義線程池擴容最大容量
    private final static int MAXZISE=30;

    //可提交任務的最大值
    private final int taskQueueMaxSize;

    //線程池默認大小
    public final static int DEFAULT_SIZE=10;

    //任務隊列,用於存放外部傳進來的所有任務,執行一個就刪除一個
    private final static LinkedList<Runnable> TASK_QUEUE = new LinkedList<>();

    // 默認的任務最大個數
    public final static int DEFAULT_TASK_QUEUE_SIZE=2000;

    // 用於存放線程池的所有線程
    private final static List<WorkerTask> WORKER_THREAD_QUEUE = new ArrayList<>();

    // 用於線程命名自增
    private static volatile int seq;

    // 用於線程命名前綴
    private final static String THREAD_PREFIX = "THREAD_POOL-";

    private final static ThreadGroup GROUP = new ThreadGroup("THREAD_POOL");

    // 默認的拒絕策略,超過數量直接拋出異常
    public final static DiscardPolicy DEFAULT_DISCARD_POLICY = ()->{
        throw new DiscardException("提交任務數量過多,任務被拒絕!");
    };
    // 提供給外部傳入
    private DiscardPolicy discardPolicy;

    // 線程池的狀態
    private boolean isDead;

    //空構造,設爲默認大小
    public ThreadPool(){
        this(DEFAULT_SIZE,DEFAULT_TASK_QUEUE_SIZE,DEFAULT_DISCARD_POLICY);
    }
    public ThreadPool(int size, int taskQueueMaxSize, DiscardPolicy discatdPolicy){
        this.size=size;
        this.taskQueueMaxSize = taskQueueMaxSize;
        this.discardPolicy = discatdPolicy;
        // 初始化線程池
        init();
    }

    private void init() {
        // 初始化創建size個工作線程
        for (int i=0;i<size;i++){
            createWorkTask();
        }
        this.start();
    }

    // 對外提供接口,加入任務
    public void submit(Runnable runnable){
        // 隊列入口,搶鎖
        synchronized (TASK_QUEUE){
            // 拒絕策略判斷,任務隊列的個數大於最大值
            if (TASK_QUEUE.size()> taskQueueMaxSize){
                discardPolicy.discard();
            }
            TASK_QUEUE.addLast(runnable);
            // 加入隊列之後就通知其他休眠的(類似生產者消費者模型)
            TASK_QUEUE.notifyAll();
        }
    }
    // 線程池的狀態監控:擴容、縮容等
    @Override
    public void run(){
        while (!isDead){
            // 擴容
            extend();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 縮容
            unextend();
        }
    }

    // 擴容
    private void extend(){
        synchronized (WORKER_THREAD_QUEUE){
            // 如果提交的任務個數TASK_QUEUE.size()/2 > WROKER_THREAD_QUEUE,就再創建一個工作線程
            if (TASK_QUEUE.size()<<1 > WORKER_THREAD_QUEUE.size() && WORKER_THREAD_QUEUE.size()<MAXZISE){
                System.out.println("擴容前:size:"+size);
                createWorkTask();
                size++;
                System.out.println("擴容後:size:"+size);
            }
        }
    }

    //縮容
    private void unextend(){
        synchronized (WORKER_THREAD_QUEUE){
            // 如果提交的任務個數TASK_QUEUE.size()/2 < WROKER_THREAD_QUEUE,就減少一個工作線程
            if (TASK_QUEUE.size()<<1 < WORKER_THREAD_QUEUE.size() && TASK_QUEUE.size()>0){
                System.out.println("縮容前:size:"+size);
                for (Iterator<WorkerTask> iterator = WORKER_THREAD_QUEUE.iterator();iterator.hasNext();){
                    WorkerTask task = iterator.next();
                    if (task.state==TaskState.BLOCKED){
                        task.interrupt();
                        task.close();
                        size--;
                        System.out.println("縮容後:size:"+size);
                    }
                }

            }
        }
    }


    // 創建自定義的工作線程
    private void createWorkTask(){
        WorkerTask task = new WorkerTask(GROUP,THREAD_PREFIX+(seq++));
        // 線程池中的線程在線程池創建後就被啓動,具體的狀態根據任務需求會自動更改
        task.start();
        // 加到工作隊列中
        WORKER_THREAD_QUEUE.add(task);
    }


    // 拒絕策略,提供接口,讓外部也能實現自定義拒絕策略
    public interface DiscardPolicy{
        void discard() throws DiscardException;
    }
    // 通過拋出異常,拋出拒絕
    public static class DiscardException extends RuntimeException{
        public DiscardException(String msg){
            super(msg);
        }
    }

    // 關閉線程池
    public void shutdown() throws InterruptedException {
        // 如果任務隊列還有,那就稍等一會
        while (!TASK_QUEUE.isEmpty()){
            Thread.sleep(1);
        }
        // 如果任務隊列裏面沒有任務了,那就結束,需要循環結束
        int initVal = WORKER_THREAD_QUEUE.size();
        while (initVal>0){
            for (WorkerTask task :
                    WORKER_THREAD_QUEUE) {
                //如果任務執行完畢,那麼狀態必爲阻塞
                if (task.state==TaskState.BLOCKED) {
                    task.close(); //state設爲DADE
                    task.interrupt();//打斷,退出任務循環
                    initVal--;
                    // 如果不是,就先等待一下,不要瘋狂運行
                }else {
                    System.out.println(TASK_QUEUE.size());
                    Thread.sleep(1);
                }
            }
        }
        isDead=true;
        System.out.println("--------------線程池已被關閉----------------");
    }


    // 定義任務狀態,空閒、運行、阻塞、死亡
    private enum TaskState {
        FREE,RUNNING,BLOCKED,DEAD
    }
    // 封裝Thread對象,讓其擁有我們定義的任務狀態等其他信息
    private static class WorkerTask extends Thread{

        //初始化爲空閒狀態
        private volatile TaskState state = TaskState.FREE;

        // 獲取任務狀態
        public TaskState getTaskState(){
            return this.state;
        }

        //調用父類的group的構造方法
        public WorkerTask(ThreadGroup group,String name){
            super(group,name);
        }
        //run,讓其執行完之後不能銷燬,而是放回池中
        @Override
        public void run() {
            //如果線程狀態沒有死亡,就去隊列中獲取任務,保證工作線程在系統運行期間永不消亡
            OUTER:
            while (this.state!=TaskState.DEAD){
                // 聲明任務
                Runnable runnable;
                //出隊,搶鎖
                synchronized (TASK_QUEUE){
                    // 如果隊列爲空,就讓線程等待,釋放鎖
                    while (TASK_QUEUE.isEmpty()){
                        try {
                            // 進入wait,修改狀態
                            state = TaskState.BLOCKED;
                            TASK_QUEUE.wait();
                        } catch (InterruptedException e) {
                            //如果線程被打斷,就重新去獲取任務,保證工作線程永不消亡
                            break OUTER;
                        }
                    }
                    // 如果隊列不爲空,就拿到第一個任務
                    runnable = TASK_QUEUE.removeFirst();
                    }
                // 拿到任務釋放鎖,開始工作
                if (runnable!=null){
                    // 開始執行,修改狀態
                    state = TaskState.RUNNING;
                    runnable.run();
                    // 執行完畢,修改狀態
                    state = TaskState.FREE;
                }
            }
        }

        // 關閉任務
        public void close(){
            this.state = TaskState.DEAD;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 初始化一個線程池
        ThreadPool pool = new ThreadPool();
        // 提交40個任務
        for (int i = 0; i < 40; i++) {
            pool.submit(()->{
                System.out.println("任務被線程"+Thread.currentThread().getName()+"執行");
                try {
                    // 模擬工作
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任務被線程"+Thread.currentThread().getName()+"執行完畢");
            });
        }
        // 等待線程工作
        Thread.sleep(20000);
        pool.shutdown();
    }
}

未完待續

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