1. 程式人生 > 實用技巧 >一鍵智慧摳圖-原理與實現

一鍵智慧摳圖-原理與實現

1 併發與並行

併發:指兩個或多個事件在同一個時間段內發生。

並行:指兩個或多個事件在同一時刻發生(同時發生)。

併發指的是在一段時間內巨集觀上有多個程式同時執行:

  • 在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替執行,只不過是給人的感覺是同時執行,那是因為分時交替執行的時間是非常短的。
  • 在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多工並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核CPU,便是多核處理器,核越多,並行處理的程式越多,能大大的提高電腦執行的效率。

注意:單核處理器的計算機肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發執行。同理,執行緒也是一樣的,從巨集觀角度上理解執行緒是並行執行的,但是從微觀角度上分析卻是序列執行的,即一個執行緒一個執行緒的去執行,當系統只有一個CPU時,執行緒會以某種順序執行多個執行緒,我們把這種情況稱之為執行緒排程。

2 執行緒與程序

程式:指令和資料的有序集合,本身沒任何執行含義,是靜態概念

程序:是指一個記憶體中執行的應用程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時執行多個程序;程序也是程式的一次執行過程,是系統執行程式的基本單位;系統執行一個程式即是一個程序從建立、執行到消亡的過程。(進入記憶體中執行的程式,叫程序)

執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。一個程序中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。

簡而言之:一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒

執行緒排程:

  • 分時排程

​ 所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。

  • 搶佔式排程

​ 優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性);

​ Java使用的為搶佔式排程。

多執行緒程式並不能提高程式的執行速度,但能夠提高程式執行效率,讓CPU的使用率更高。

在程式執行時,即使沒有自己建立執行緒,後臺也會有多個執行緒,如主執行緒、gc執行緒

main()執行緒稱之為主執行緒,為系統的入口,用於執行整個程式

在一個程序中,如果開闢了多執行緒,執行緒的執行由排程器安排排程,排程器是與作業系統緊密相關的,先後順序是不能人為的干預

對同一份資源操作時,會出現資源搶奪問題,需加入併發控制

執行緒會帶來額外的開銷,如cpu排程時間,併發控制開銷

每個執行緒在自己的工作記憶體互動,記憶體控制不當會造成資料不一致

3 執行緒

3.1 Thread類

Java使用java.lang.Thread 類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的例項。每個執行緒的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的程式碼。Java使用執行緒執行體來代表這段程式流。

構造方法

  • public Thread() :分配一個新的執行緒物件。
  • public Thread(String name) :分配一個指定名字的新的執行緒物件。
  • public Thread(Runnable target) :分配一個帶有指定目標新的執行緒物件。
  • public Thread(Runnable target,String name) :分配一個帶有指定目標新的執行緒物件並指定名字。

常用方法

  • public String getName() :獲取當前執行緒名稱。
  • public void start() :導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法。
  • public void run() :此執行緒要執行的任務在此處定義程式碼。
  • public static void sleep(long millis) :使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行)。
  • public static Thread currentThread() :返回對當前正在執行的執行緒物件的引用。

主執行緒

JVM執行main方法,main方法會進入到棧記憶體,JVM會找作業系統開闢一條 main方法通向CPU的執行路徑,CPU就可以通過這個路徑來執行main方法,而此路徑就稱為main(主)執行緒。

  • 普通方法呼叫和多執行緒呼叫

3.2 建立執行緒

3.2.1 方式一:繼承Thread類

Java中通過繼承Thread類來建立並啟動多執行緒的步驟如下:

  • 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了執行緒需要完成的任務,因此把run()方法稱為執行緒執行體。
  • 建立Thread子類的例項,即建立了執行緒物件
  • 呼叫執行緒物件的start()方法來啟動該執行緒
  • 啟動執行緒後,不一定立即執行,由cpu排程安排(主執行緒與thread執行緒,穿插進行)
  • 子類繼承Thread類具有多執行緒的能力(Thread類實現類Runnable介面)
  • 啟動執行緒:子類物件.start()
  • 不建議使用:避免OOP單繼承侷限性
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "正在執行!" + i);
        }
    }
}
public class DemoTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread("新的執行緒");
        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main執行緒" + i);
        }
    }
}

3.2.2 方式二:實現Runnable介面

實現java.lang.Runnable介面,重寫run()方法。(Thread類也實現了Runnable介面)

步驟:

  • 定義Runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
  • 建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
  • 呼叫執行緒物件的start()方法來啟動執行緒。
  • 實現Runnable具有多執行緒能力
  • 啟動執行緒:傳入目標物件+Thread物件.start()
  • 推薦:避免單繼承侷限性,方便同一個物件被多個執行緒使用
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.print(Thread.currentThread().getName()+ " " + i + ",");
        }
    }
}
public class DemoTest {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        Thread tr = new Thread(mr, "小強");
        Thread otr = new Thread(mr, "佩奇");
        tr.start();
        otr.start();
        for (int i = 0; i < 10; i++) {
            System.out.print("旺財" + i + ",");
        }
    }
}
/**
旺財0,旺財1,旺財2,旺財3,旺財4,旺財5,旺財6,旺財7,旺財8,旺財9,
佩奇 0,小強 0,佩奇 1,小強 1,佩奇 2,小強 2,佩奇 3,小強 3,佩奇 4,
小強 4,佩奇 5,小強 5,小強 6,小強 7,小強 8,小強 9,佩奇 6,佩奇 7,佩奇 8,佩奇 9,
*/

**所有的多執行緒程式碼都在run方法裡面

tips:Runnable物件僅僅作為Thread物件的target,Runnable實現類裡包含的run()方法僅作為執行緒執行體。而實際的執行緒物件依然是Thread例項,只是該Thread執行緒負責執行其target的run()方法。

3.2.3 方式三:匿名內部類方式實現執行緒建立

使用執行緒的內匿名內部類方式,可以方便的實現每個執行緒執行不同的執行緒任務操作。

使用匿名內部類的方式實現Runnable介面,重新Runnable介面中的run方法:

public class NoNameInnerClassThread {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("xiaoqinag" + i);
                }
            }
        };
        new Thread(runnable).start();
        for (int i = 0; i < 20; i++) {
            System.out.println("daqiang:"+i);
        }
    }
}

3.2.4 方式四:Lambda方式實現執行緒建立

public class LambdaThread {
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("xiaoqinag" + i);
            }
        }, "lambdaThread").start();

        for (int i = 0; i < 20; i++) {
            System.out.println("daqiang:"+i);
        }
    }
}

3.2.5 實現Callable介面

  • 實現Callable介面
    • 1.實現Callable介面,需要返回值型別
    • 2.重寫call方法,需要丟擲異常
    • 3.建立目標物件t1
    • 4.建立執行服務: ExecutorService ser = Executors.newFixedThreadPool(1);
    • 5.提交執行: Future<Boolean> result1 = ser.submit(t1);
    • 6.獲取結果: boolean r1 = result1.get()

3.2.6 Thread和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。

實現Runnable介面比繼承Thread類所具有的優勢:

  • 適合多個相同的程式程式碼的執行緒去共享同一個資源。

  • 可以避免java中的單繼承的侷限性。

  • 增加程式的健壯性,實現解耦操作,程式碼可以被多個執行緒共享,程式碼和執行緒獨立。

    • 實現Runnable介面的方式,把設定執行緒任務和開啟新執行緒進行分解(解耦)
    • 實現類中重寫run方法:用來設定執行緒的任務;
    • 建立Thread類物件,呼叫start方法:用來開啟新執行緒
  • 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類。

擴充:在java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個程序。

3.3 多執行緒的原理

public class MyThread extends Thread{
    /*
* 利用繼承中的特點
* 將執行緒名稱傳遞 進行設定
*/
    public MyThread(String name){
        super(name);
    }
    /*
* 重寫run方法
* 定義執行緒要執行的程式碼
*/
    public void run(){
        for (int i = 0; i < 20; i++) {
            //getName()方法 來自父親
            System.out.println(getName()+i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        System.out.println("這裡是main執行緒");
        MyThread mt = new MyThread("小強");
        mt.start();//開啟了一個新的執行緒
        for (int i = 0; i < 20; i++) {
            System.out.println("旺財:"+i);
        }
    }
}   

程式啟動執行main時候,java虛擬機器啟動一個程序,主執行緒main在main()呼叫時候被建立。隨著呼叫mt的物件的start方法,另外一個新的執行緒也啟動了,這樣,整個應用就在多執行緒下執行。2個執行緒搶佔CPU的執行資源

多執行緒執行時,在棧記憶體中,其實每一個執行執行緒都有一片自己所屬的棧記憶體空間

多個執行緒之間互不影響(在不同的棧空間)

3.4 守護(daemon)執行緒

  • 執行緒分為使用者執行緒和守護執行緒
  • 虛擬機器必須確保使用者執行緒執行完畢
  • 虛擬機器不用等待守護執行緒執行完畢
  • 使用者執行緒結束,守護執行緒也會停止
  • 使用者執行緒變守護執行緒(setDaemon(true),預設為false)
public class DaemonThread {
    public static void main(String[] args) {
        //守護執行緒
        Thread god = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName() + ":god...always");
            }
        }, "God thread");
        god.setDaemon(true);
        god.start();
		//使用者執行緒
        new Thread(() -> {
            for (int i = 0; i < 36500; i++) {
                System.out.println(Thread.currentThread().getName() + ":live" + i);
            }
            System.out.println("======GOODBYE WORLD=====");
        }, "You thread").start();
    }
}

4 執行緒安全

4.1 執行緒安全

如果有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。程式每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

/** 模擬多視窗買票 */
public class Ticket implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {//模擬出票
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--);
            }
        }
    }
}
public static void main(String[] args) {
    Ticket ticket = new Ticket();
    Thread t1 = new Thread(ticket, "視窗1");
    Thread t2 = new Thread(ticket, "視窗2");
    Thread t3 = new Thread(ticket, "視窗3");
    t1.start();
    t2.start();
    t3.start();
}
/**
...
視窗1正在賣:3
視窗2正在賣:2
視窗3正在賣:1
視窗1正在賣:0
視窗2正在賣:-1
*/

執行緒安全問題都是由全域性變數及靜態變數引起的。若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

4.2 執行緒同步

當使用多個執行緒訪問同一資源的時候,且多個執行緒中對資源有寫的操作,就容易出現執行緒安全問題。

要解決上述多執行緒併發訪問一個資源的安全性問題,Java中提供了同步機制****(synchronized)來解決。

執行緒同步其實就是一種等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用

實現同步機制的三種方式:(同步條件:佇列+鎖)

  • 同步程式碼塊。
  • 同步方法。
  • 鎖機制。

加鎖後存在的問題:

  • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
  • 在多執行緒競爭下,加鎖、釋放鎖,會導致比較多的上下文切換 和 排程延時,引起效能問題
  • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題

4.2.1 同步程式碼塊

同步程式碼塊: synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。

格式:

synchronized(同步鎖Obj){
    //需要同步操作的程式碼
}

同步鎖(同步監視器):

物件的同步鎖只是一個概念,可以想象為在物件上標記了一個鎖.

  • 鎖物件obj 可以是任意型別,推薦使用共享資源作為同步監視器。
  • 多個執行緒物件 要使用同一把鎖。

注意:在任何時候,最多允許一個執行緒擁有同步鎖,誰拿到鎖就進入程式碼塊,其他的執行緒只能在外等著(BLOCKED)。

public class Ticket implements Runnable {
    private int ticket = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {//鎖
                if (ticket > 0) {
                    try {//模擬出票
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--);
                }
            }
        }
    }
}

4.2.2 同步方法

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A執行緒執行該方法的時候,其他執行緒只能在方法外等著。

格式

public synchronized void method(){
    //可能會產生執行緒安全問題的程式碼
}

同步鎖是誰?

  • 對於非static方法,同步鎖就是this。
  • 對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)。
@Override
public void run() {
    while (true) {
        sellTicket();
    }
}
/**
 * 鎖物件 是 誰呼叫這個方法 就是誰
 * 隱含 鎖物件 就是 this
 */
private synchronized void sellTicket() {
    if (ticket > 0) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--);
    }
}

4.2.3 鎖機制Lock

java.util.concurrent.locks.Lock 機制提供了比synchronized程式碼塊和synchronized方法更廣泛的鎖定操作,同步程式碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向物件。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

  • public void lock() :加同步鎖。
  • public void unlock() :釋放同步鎖。(放在finally中進行)
public class LockThread implements Runnable {
    Lock lock = new ReentrantLock();//ReentrantLock 實現了 Lock 介面
    @Override
    public void run() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

4.2.4 synchronize && lock

  • Lock是顯式鎖(手動開啟和關閉鎖),synchronize是隱式鎖,出了作用域自動釋放
  • Lock只有程式碼塊鎖,synchronize有程式碼塊鎖和方法鎖
  • 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好,並且具有更好的擴充套件性(提供更多子類)
  • 優先使用順序
    • Lock > 同步程式碼塊(已進入方法體,分配了相應資源) > 同步方法(在方法體外)

5 執行緒狀態

5.1 執行緒狀態概述

在API中java.lang.Thread.State 這個列舉中給出了六種執行緒狀態

執行緒狀態 導致狀態發生條件
NEW(新建) 執行緒剛被建立,但是並未啟動。還沒呼叫start方法。
Runnable(可執行) 執行緒可以在java虛擬機器中執行的狀態,可能正在執行自己程式碼,也可能沒有,這取決於作業系統處理器。
Blocked(鎖阻塞) 當一個執行緒試圖獲取一個物件鎖,而該物件鎖被其他的執行緒持有,則該執行緒進入Blocked狀態;當該執行緒持有鎖時,該執行緒將變成Runnable狀態。
Waiting(無限等待) 一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒。
Timed Waiting(計時等待) 同waiting狀態,有幾個方法有超時引數,呼叫他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時引數的常用方法有Thread.sleep 、Object.wait。
Teminated(被終止) 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

Timed Waiting:A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.

  • 進入 TIMED_WAITING 狀態的一種常見情形是呼叫的 sleep 方法,單執行緒也可以呼叫,不一定非要有協作關係。
  • 為了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣能保證該執行緒執行過程中會睡眠
  • sleep與鎖無關,執行緒睡眠到期自動甦醒,並返回到Runnable(可執行)狀態。

小提示:sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就開始立刻執行。

Blocked:A thread that is blocked waiting for a monitor lock is in this state.

  • 受阻塞並且正在等待監視器鎖的某一執行緒的執行緒狀態。處於受阻塞狀態的某一執行緒正在等待監視器鎖,以便進入一個同步的塊/方法;
  • 執行緒A與執行緒B程式碼中使用同一鎖,如果執行緒A獲取到鎖,執行緒A進入到Runnable狀態,那麼執行緒B就進入到Blocked鎖阻塞狀態。

Waiting : A thread that is waiting indefinitely for another thread to perform a particular action is in this state.

  • 一個正在無限期等待另一個執行緒執行一個特別的(喚醒)動作的執行緒處於這一狀態。
public class WaitingDemo {
    public static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (obj) {
                        try {
                            System.out.println(Thread.currentThread().getName()
                                + "=== 獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件");
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "=== 從waiting狀態醒來,獲取到鎖物件,繼續執行了");
                    }
                }
            }
        },"等待執行緒").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println(Thread.currentThread().getName()
                                + "------- 等待3秒鐘");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj) {
                        System.out.println(Thread.currentThread().getName()
                                + "------ 獲取到鎖物件,呼叫notify方法,釋放鎖物件");
                        obj.notify();
                    }
                }
            }
        },"喚醒執行緒").start();
    }
}
/**
等待執行緒=== 獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件
喚醒執行緒------- 等待3秒鐘
喚醒執行緒------ 獲取到鎖物件,呼叫notify方法,釋放鎖物件
喚醒執行緒------- 等待3秒鐘
等待執行緒=== 從waiting狀態醒來,獲取到鎖物件,繼續執行了
等待執行緒=== 獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件
喚醒執行緒------ 獲取到鎖物件,呼叫notify方法,釋放鎖物件
喚醒執行緒------- 等待3秒鐘
等待執行緒=== 從waiting狀態醒來,獲取到鎖物件,繼續執行了
等待執行緒=== 獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件
喚醒執行緒------ 獲取到鎖物件,呼叫notify方法,釋放鎖物件
喚醒執行緒------- 等待3秒鐘
等待執行緒=== 從waiting狀態醒來,獲取到鎖物件,繼續執行了
等待執行緒=== 獲取到鎖物件,呼叫wait方法,進入waiting狀態,釋放鎖物件
...
*/
  • 一個呼叫了某個物件的 Object.wait 方法的執行緒會等待另一個執行緒呼叫此物件的Object.notify()方法 或 Object.notifyAll()方法。其實waiting狀態並不是一個執行緒的操作,它體現的是多個執行緒間的通訊,可以理解為多個執行緒之間的協作關係,多個執行緒會爭取鎖,同時相互之間又存在協作關係
  • 當多個執行緒協作時,比如A,B執行緒,如果A執行緒在Runnable(可執行)狀態中呼叫了wait()方法那麼A執行緒就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B執行緒獲取到了同步鎖,在執行狀態中呼叫了notify()方法,那麼就會將無限等待的A執行緒喚醒。注意是喚醒,如果獲取到鎖物件,那麼A執行緒喚醒後就進入Runnable(可執行)狀態;如果沒有獲取鎖物件,那麼就進入到Blocked(鎖阻塞狀態)。

執行緒個狀態之間的轉化:

public class StateThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("AAAAAAAAAAAAAAAAAA");
        });

        //NEW
        Thread.State state = thread.getState();
        System.out.println(state);
        thread.start();
        //RUNNABLE
        state = thread.getState();
        System.out.println(state);

        while (state != Thread.State.TERMINATED) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //TIMED_WAITING
            System.out.println(state);
            state = thread.getState();
        }
        //TERMINATED
        System.out.println(state);
    }
}

5.2 執行緒的停止

  • 執行緒執行完後自己停止
  • 使用標誌位控制執行緒停止(推薦)
  • 不推薦jdk提供的stop、destroy方法(已廢棄)
public class StopThread implements Runnable {
    //標誌位
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            System.out.println("thread....run...");
        }
    }

    //對外提供改變標誌位方法
    public void stop() {
        this.flag = false;
    }

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

        for (int i = 0; i < 1000; i++) {
            System.out.println("main.....run..." + i);
            if (i == 988) {
                target.stop();
                System.out.println("thread....stop...");
            }
        }
    }
}

5.3 執行緒休眠

  • sleep(時間) 指定當前執行緒阻塞的毫秒數
  • sleep存在異常 InterruptException
  • sleep時間到達後執行緒進入就緒狀態
  • sleep模擬網路延時(放大問題發生的可能性)、倒計時等
  • 每一個物件都有一個鎖,sleep不會釋放鎖
public class SleepThread implements Runnable {
    private int ticketNum = 10;

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                break;
            }
            try {
                //模擬網路延時
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "張票");
        }
    }

    public static void main(String[] args) {
        TestTicketThread ticket = new TestTicketThread();
        new Thread(ticket, "AA").start();
        new Thread(ticket, "BB").start();
        new Thread(ticket, "CC").start();

        oneMinDown();
    }

    //倒計時
    public static void oneMinDown() {
        int num = 60;
        for (int i = num; i > 0; i--) {
            System.out.println(num--);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5.4 執行緒禮讓

  • 禮讓執行緒yield,讓當前正在執行的執行緒暫停,但不阻塞
  • 將執行緒從執行狀態轉為就緒狀態
  • 讓cpu重新排程,禮讓不一定成功
public class YieldThread {
    public static void main(String[] args) {
        Runnable target = ()->{
            System.out.println(Thread.currentThread().getName()+"-->start");
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"-->end");
        };

        new Thread(target,"AA").start();
        new Thread(target,"BB").start();
    }
}
/*
禮讓成功					 禮讓失敗
AA-->start					AA-->start
BB-->start					AA-->end
AA-->end					BB-->start
BB-->end					BB-->end
*/

5.5 執行緒強制執行

  • join 合併執行緒,阻塞其他執行緒,待該執行緒執行完之後,再執行其他執行緒
  • 類似插隊
public class JoinThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("thread...."+i);
            }
        });
        thread.start();

        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                try {
                    //插隊
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main...." + i);
        }
    }
}

5.6 執行緒優先順序

  • Java提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行
  • 執行緒優先順序用數字表示,範圍1~10
    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5;
  • getPriority() 獲取優先順序
  • setPriority(int xxx) 設定優先順序 優先順序的設定在start()之前
  • 優先順序高低意味著獲得優先排程概率的高低,最終的排程還是看cpu

6 等待喚醒機制

6.1 執行緒間通訊

概念:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。

  • 為何要處理執行緒之間的通訊?

​ 讓多執行緒在訪問同一份資源時按照一定的規律進行。

  • 如何保證執行緒間通訊有效利用資源:

​ 多個執行緒在處理同一個資源,並且任務不同時,需要執行緒通訊來幫助解決執行緒之間對同一個變數的使用或操作,避免對同一共享變數的爭奪————等待喚醒機制

6.2 等待喚醒機制

等待喚醒機制

  • 是多個執行緒間的一種協作機制
  • 在一個執行緒進行了規定操作後,就進入等待狀態(wait()), 等待其他執行緒執行完他們的指定程式碼過後 再將其喚醒(notify());在有多個執行緒進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待執行緒。
  • wait/notify 就是執行緒間的一種協作機制。

等待喚醒中的方法

  • wait:執行緒不再活動,不再參與排程,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的執行緒狀態即是 WAITING。它還要等著別的執行緒執行一個特別的動作,也即是“通知(notify)”在這個物件上等待的執行緒從wait set 中釋放出來,重新進入到排程佇列(ready queue)中
  • wait(long timeout):等待指定的毫秒數
  • notify:則選取所通知物件的 wait set 中的一個執行緒釋放;例如,餐館有空位後,等候就餐最久的顧客最先入座。
  • notifyAll:則釋放所通知物件的 wait set 上的全部執行緒,優先級別高的執行緒優先排程。

注意:

哪怕只通知了一個等待的執行緒,被通知執行緒也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它執行緒的競爭),成功後才能在當初呼叫 wait 方法之後的地方恢復執行。

總結如下:

  • 如果能獲取鎖,執行緒就從 WAITING 狀態變成 RUNNABLE 狀態;
  • 否則,從 wait set 出來,又進入 entry set,執行緒就從 WAITING 狀態又變成 BLOCKED 狀態

呼叫wait和notify方法需要注意的細節

  • wait方法與notify方法必須要由同一個鎖物件呼叫。因為:對應的鎖物件可以通過notify喚醒使用同一個鎖物件呼叫的wait方法後的執行緒。
  • wait方法與notify方法是屬於Object類的方法的。因為:鎖物件可以是任意物件,而任意物件的所屬類都是繼承了Object類的。
  • wait方法與notify方法必須要在同步程式碼塊或者是同步函式中使用,否則會丟擲異常IIlegalMonitorStateException。因為:必須要通過鎖物件呼叫這2個方法。

6.3 生產者與消費者問題

等待喚醒機制其實就是經典的“生產者與消費者”的問題。

生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件

  • 對於生產者,沒有生產產品之前,要通知消費者等待。而生產產品之後,又要馬上通知消費者消費
  • 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費

解決方法:執行緒同步+執行緒通訊

6.3.1 訊號燈法(通過標誌位)

  • 包子鋪執行緒生產包子,吃貨執行緒消費包子。當包子沒有時(包子狀態為false),吃貨執行緒等待,包子鋪執行緒生產包子(即包子狀態為true),並通知吃貨執行緒(解除吃貨的等待狀態),因為已經有包子了,那麼包子鋪執行緒進入等待狀態。接下來,吃貨執行緒能否進一步執行則取決於鎖的獲取情況。如果吃貨獲取到鎖,那麼就執行吃包子動作,包子吃完(包子狀態為false),並通知包子鋪執行緒(解除包子鋪的等待狀態),吃貨執行緒進入等待。包子鋪執行緒能否進一步執行則取決於鎖的獲取情況。
//資源
public class Baozi {
    private String pier;
    private String xianer;
    private boolean flag = false;//包子資源,是否存在
    public Baozi() {
    }
    public Baozi(String pier, String xianer) {
        this.pier = pier;
        this.xianer = xianer;
    }
    public String getPier() {
        return pier;
    }
    public void setPier(String pier) {
        this.pier = pier;
    }
    public String getXianer() {
        return xianer;
    }
    public void setXianer(String xianer) {
        this.xianer = xianer;
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
//消費者
public class ChiHuo extends Thread {
    private Baozi bz;
    public ChiHuo(String name, Baozi bz) {
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while (true) {
            synchronized (bz) {
                if (bz.isFlag() == false) {
                    try {
                        bz.wait();//吃貨等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃貨正在吃" + bz.getPier() + bz.getXianer() + "包子!");
                System.out.println("包子吃完了!");
                bz.setFlag(false);
                bz.notify();//喚醒包子鋪
            }
        }
    }
}
//生產者
public class BaoZiPu extends Thread {
    private Baozi bz;
    public BaoZiPu(String name, Baozi bz) {
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        int count = 0;
        while (true) {
            synchronized (bz) {
                if (bz.isFlag()) {
                    try {
                        bz.wait();//包子鋪停止生產
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("包子鋪開始做包子");
                if (count % 2 == 0) {
                    bz.setPier("冰皮");
                    bz.setXianer("五仁");
                } else {
                    bz.setPier("薄皮");
                    bz.setXianer("牛肉大蔥");
                }
                count++;
                bz.setFlag(true);
                System.out.println("包子造好了:" + bz.getPier() + bz.getXianer());
                System.out.println("吃貨來吃包子吧");
                bz.notify();//喚醒吃貨吃包子
            }
        }
    }
}
public class BaoZiTest {
    public static void main(String[] args) {
        Baozi bz = new Baozi();
        BaoZiPu bzp = new BaoZiPu("包子鋪", bz);
        ChiHuo ch = new ChiHuo("吃貨", bz);
        bzp.start();
        ch.start();
    }
}
/*
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃包子吧
吃貨正在吃冰皮五仁包子!
包子吃完了!
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃包子吧
吃貨正在吃薄皮牛肉大蔥包子!
包子吃完了!
包子鋪開始做包子
*/

6.3.2 管程法

  • 生產者:負責生產資料的模組(可能是方法、物件、執行緒、程序)

  • 消費者:負責處理資料的模組(可能是方法、物件、執行緒、程序)

  • 緩衝區:消費者不能直接使用生產者的資料,他們之間有個“緩衝區”,生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料

//生產者、消費者、產品、容器
public class PCThread {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Provider(container).start();
        new Consumer(container).start();
    }
}

//生產者
class Provider extends Thread {
    SynContainer container;

    public Provider(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("生產者生產第" + i + "只雞");
            container.push(new Chicken(i));
        }
    }
}

//消費者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消費者消費第" + container.pop().id + "只雞");
        }

    }
}

//資源 雞
class Chicken {
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}

//緩衝區 容器
class SynContainer {
    //容器大小
    Chicken[] chickens = new Chicken[10];
    //容器計數器
    int count;

    //生產者放入產品
    public synchronized void push(Chicken chicken) {
        //容器滿了,生產者停止生產,等待消費者消費
        if (count == chickens.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //沒滿,則放入產品
        chickens[count] = chicken;
        count++;
        //通知消費者消費
        this.notifyAll();
    }

    //消費者消費產品
    public synchronized Chicken pop() {
        //判斷能否消費
        if (count == 0) {
            //消費者等待生產者生產
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //可以消費
        count--;
        Chicken chicken = chickens[count];

        //吃完了通知生產者生產
        this.notifyAll();
        return chicken;
    }
 }

7 執行緒池

7.1 概述

執行緒池:其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。

  • 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,最後宕機)。
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大執行緒數
    • keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止

7.2 執行緒池的使用

  • Java裡面執行緒池的頂級介面是java.util.concurrent.Executor ,但是嚴格意義上講Executor 並不是一個執行緒池,而只是一個執行執行緒的工具。真正的執行緒池介面是java.util.concurrent.ExecutorService

  • 在java.util.concurrent.Executors 執行緒工廠類裡面提供了一些靜態工廠,生成一些常用的執行緒池。官方建議使用Executors工程類來建立執行緒池物件。

    • public static ExecutorService newFixedThreadPool(int nThreads) :返回執行緒池物件。(建立的是有界執行緒池,也就是池中的執行緒個數可以指定最大數量)
  • 使用執行緒池物件

    • void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable

    • <T> Future<T> submit(Callable<T> task):執行任務,有返回值,一般用來執行Callable

      • Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用。
    • void shutdown():關閉連線池

  • 使用執行緒池中執行緒物件的步驟:

    • 建立執行緒池物件。
    • 建立Runnable介面子類物件。(task)
    • 提交Runnable介面子類物件。(take task)
    • 關閉執行緒池(一般不做)。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一個教練");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教練來了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完後,教練回到了游泳池");
    }
}
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 建立執行緒池物件
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個執行緒物件
        // 建立Runnable例項物件
        MyRunnable r = new MyRunnable();
        //自己建立執行緒物件的方式
        // Thread t = new Thread(r);
        // t.start(); ‐‐‐> 呼叫MyRunnable中的run()
        // 從執行緒池中獲取執行緒物件,然後呼叫MyRunnable中的run()
        service.submit(r);
        // 再獲取個執行緒物件,呼叫MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法呼叫結束後,程式並不終止,是因為執行緒池控制了執行緒的關閉。
        // 將使用完的執行緒又歸還到了執行緒池中
        // 關閉執行緒池
        //service.shutdown();
    }
}