Java多線程之基礎

1 并發與并行

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

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

10168484.png

并發指的是在一段時間內宏觀上有多個程序同時運行:

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

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

2 線程與進程

程序:指令和數據的有序集合,本身沒任何運行含義,是靜態概念

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

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

11978546.png

簡而言之:一個程序運行后至少有一個進程,一個進程中可以包含多個線程

線程調度:

  • 分時調度

? 所有線程輪流使用 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(主)線程。

  • 普通方法調用和多線程調用


    1607496016082.png

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);
        }
    }
}   
13524109.png

程序啟動運行main時候,java虛擬機啟動一個進程,主線程main在main()調用時候被創建。隨著調用mt的對象的start方法,另外一個新的線程也啟動了,這樣,整個應用就在多線程下運行。2個線程搶占CPU的執行資源

多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間

13778937.png

多個線程之間互不影響(在不同的棧空間)

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 線程狀態概述

1607496905497.png

在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(鎖阻塞狀態)。
30979078.png

線程個狀態之間的轉化:

13316703.png
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 管程法

  • 生產者:負責生產數據的模塊(可能是方法、對象、線程、進程)

  • 消費者:負責處理數據的模塊(可能是方法、對象、線程、進程)

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

1607675140541.png
//生產者、消費者、產品、容器
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 概述

線程池:其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。

6256906.png
  • 降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
  • 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約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();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Java多線程概念 進程(Process): ?進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態...
    凌云struggle閱讀 270評論 1 0
  • ? 進程、線程和多線程 進程: 定義:進程是一個具有一定獨立功能的程序關于某個數據集合的一次運行活動。它是操作系統...
    默云客閱讀 263評論 0 0
  • Thread(耦合,不推薦) Runnable(解耦,推薦) Executors ExecutorService ...
    如果仲有聽日閱讀 719評論 0 0
  • 知識點 進程:正在運行的某個程序 如:QQ 瀏覽器系統會為這個進程分配獨立的內存資源 線程:具體執行任務的最小單位...
    莫南爵2002TQ97閱讀 169評論 1 0
  • synchronized關鍵字 Lock 接口 ReentrantLock 類 1. 線程同步問...
    如果仲有聽日閱讀 10,715評論 0 6