天天看點

Java學習記錄 Day22(線程)

文章目錄

  • ​​Day 22​​
  • ​​線程簡介​​
  • ​​線程建立​​
  • ​​線程範例:龜兔賽跑​​
  • ​​線程狀态​​
  • ​​線程方法​​
  • ​​線程同步​​
  • ​​線程死鎖​​
  • ​​線程協作——生産者消費者模式​​
  • ​​線程池​​

Day 22

2019年6月2日。

這是我學習Java的第二十二天。

這一天,我學到了以下的知識。

線程簡介

線程,是作業系統能夠進行運算排程的最小機關。它被包含在程序之中,是程序中的實際運作機關。一條線程指的是程序中一個單一順序的控制流,一個程序中可以并發多個線程,每條線程并行執行不同的任務。

在Java中,線程的執行如圖所示:

Java學習記錄 Day22(線程)

說起線程,就必須要說到程式。程式是指令和資料的有序集合,其本身沒有任何運作的含義,是一個靜态的概念。

而程序則是執行程式的一次執行過程,它是一個動态的概念,是系統資源配置設定的機關。

通常在一個程序中可以包含若幹個線程,當然一個程序中至少有一個線程,不然沒有存在的意義。線程是CPU排程和執行的機關。

線程的性質如下所述:

  • 線程就是獨立的執行路徑;
  • 在程式運作時,即使沒有自己建立線程,背景也會有多個線程,如主線程,gc線程;
  • main()稱之為主線程,為系統的入口,用于執行整個程式;
  • 在一個程序中,如果開辟了多個線程,線程的運作由排程器安排排程,排程器是與作業系統緊密相關的,先後順序是不能認為的幹預的;
  • 對同一份資源操作時,會存在資源搶奪的問題,需要加入并發控制;
  • 線程會帶來額外的開銷,如cpu排程時間,并發控制開銷。
  • 每個線程在自己的工作記憶體互動,記憶體控制不當會造成資料不一緻。

線程建立

線程的建立,在Java中,具有三種方式:

  • 繼承Thread類

    - 實作步驟:

    1.自定義線程類繼承Thread類

    2.重寫run()方法,編寫線程執行體

    3.建立線程對象,調用start()方法啟動線程

    - 範例如下:

public class TestThread extends Thread{

//自定義run方法的線程
@Override
public void run() {
    //線程執行體
    for (int i = 0; i < 200; i++) {
        System.out.println("a:" + i);
    }
}

//主線程
public static void main(String[] args) {

    //建立線程對象
    TestThread testThread1 = new TestThread1();

    //調用start方法啟動線程
    testThread1.start();

    //同時進行
    for (int i = 0; i < 3000; i++) {
        System.out.println("b:" + i);
    }
}
}      
  • 實作Runnable接口(常用)

    - 實作步驟:

    1.定義MyRunnable類實作Runnable接口

    2.實作run()方法,編寫線程執行體

    3.建立線程對象,調用start()方法啟動線程

    - 範例如下:

public class TestThread implements Runnable{
@Override
public void run() {
    //線程執行體
    for (int i = 0; i < 200; i++) {
        System.out.println("a:" + i);
    }
}

public static void main(String[] args) {
    //重點就是将runable接口實作類的對象丢入Thread構造器
    TestThread testThread3 = new TestThread3();

    Thread thread = new Thread(testThread3);

    thread.start();

    for (int i = 0; i < 3000; i++) {
        System.out.println("b:" + i);
    }
}
}      
  • 實作Callable接口

    - 實作步驟:

    1.實作Callable接口,需要傳回值類型

    2.重寫call方法,需要抛出異常

    3.建立目标對象

    4.建立執行服務:ExecutorService ser = Executors.newFixedThreadPool(1);

    5.送出執行:Future<Boolean.> result = ser.submit(t1);

    6.關閉服務:ser.shutdownNow();

    - 範例如下:

public class Demo4Thread implements Callable<Boolean> {

@Override
public Boolean call() throws Exception {
    return false;
}

public static void main(String[] args) {
    Demo4Thread demo4Thread = new Demo4Thread();

    ExecutorService ser = Executors.newFixedThreadPool(1);

    Future<Boolean> result1 = ser.submit(demo4Thread);
}

}      
  • 繼承Thread類和實作Runnable接口的差別

    - 繼承Thread類

    1.子類繼承Thread類具備多線程能力

    2.啟動線程:子類對象.start()

    3.不建議使用:避免OOP單繼承局限性

    - 實作Runnable接口

    1.實作接口Runnable具有多線程能力

    2.啟動線程:傳入目标對象+Thread對象.start()

    3.推薦使用:避免單繼承局限性,靈活友善,友善同一個對象被多個線程使用

線程範例:龜兔賽跑

需求:模仿“龜兔賽跑”的故事,在Java中用兩個線程來實作

分析:

  1. 龜兔賽跑開始
  2. 故事中是烏龜赢的,兔子需要睡覺,是以要模拟兔子睡覺
  3. 首先定義賽道距離,然後烏龜離賽道終點越來越近
  4. 判斷比賽是否結束
  5. 最終,烏龜赢得比賽
  6. 列印出勝利者

代碼如下:

public class Race implements Runnable{

    //winner:隻有一個勝利者
    private static String winner;

    @Override
    public void run() {
        //賽道
        for (int step = 1; step <= 101; step++) {

            //模拟兔子休眠
            if (Thread.currentThread().getName().equals("兔子") && step % 50 ==0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判斷比賽是否結束
            boolean flag = gameOver(step);
            if (flag){
                break;
            }
           System.out.println( Thread.currentThread().getName() + "跑了" + step +"步");
        }
    }

    //判斷比賽是否結束
    private boolean gameOver(int step){
        if (winner != null){ // 如果存在勝利者
            return true;
        }
        if (step >= 100){ // 如果跑到了終點
            winner = Thread.currentThread().getName();
            System.out.println("比賽結束");
            System.out.println("勝利者--->" + winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"兔子").start();
        new Thread(race,"烏龜").start();
    }
}      

線程狀态

線程的狀态,在Java中,共具有5種,如圖所示:

Java學習記錄 Day22(線程)

線程的狀态轉換,如圖所示:

Java學習記錄 Day22(線程)

線程方法

在Java中,線程類裡存在一些特定的方法,可以對線程進行管理,如下所示:

  • 線程停止

    雖然線程類中提供了stop()方法和destroy()方法,但是不推薦使用,而是推薦線程自己停止下來。

    若想要線程自行停止,建議使用一個标志位充當終止變量,當flag=false,則終止線程運作。

  • 線程休眠(sleep)
  • sleep(時間)指定目前線程阻塞的毫秒數;
  • sleep存在異常InterruptedException;
  • sleep時間達到後線程進入就緒狀态;
  • sleep可以模拟網絡延時,倒計時等‘’
  • 每一個對象都有一個鎖,sleep不會釋放鎖;

用線程休眠來模拟系統時間并且讓時間流動,示例如下:

public class TestSleep2 {
    public static void main(String[] args) throws InterruptedException {

        TestSleep2 testSleep2 = new TestSleep2();

        //擷取系統時間
        Date startTime = new Date(System.currentTimeMillis());

        while (true) {
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            Thread.sleep(1000);
            startTime = new Date(System.currentTimeMillis());
        }

    }

    //倒計時方法
    private void tenDown() throws InterruptedException {
        int num = 10;
        for (int i = 10; i > 0; i--) {
            Thread.sleep(1000);
            System.out.println("倒計時:"+i);
        }

    }
}      
  • 線程禮讓(yield)
  • 禮讓線程,讓目前正在執行的線程暫停,但不阻塞;
  • 将線程從運作狀态轉為就緒狀态;
  • 讓cpu重新排程,禮讓不一定會成功!

示例如下:

public class TestYield {

    public static void main(String[] args) throws InterruptedException {
        MyYield myYield = new MyYield();
        new Thread(myYield,"小明").start();
        new Thread(myYield,"老師").start();
    }
}

class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"-->啟動了");
        Thread.yield();//禮讓
        System.out.println(Thread.currentThread().getName()+"-->停止了");
    }
}      
  • 線程插隊(join)
  • Join可以插入線程,當該線程執行完畢後,再執行其他線程(其他線程之前會阻塞);
  • join與現實世界的插隊類似;

    示例如下:

public class TestJoin implements Runnable{

    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();

        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 100; i++) {
            if (i==80){
                //強制執行
                thread.join();
            }
            System.out.println("我是主線程:"+i);
        }

    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("我是要插隊的線程:"+i);
        }
    }
}      
  • 線程狀态觀測(State)

    通過Thread.State()方法,可以檢視線程目前的狀态。

    線程的狀态,有以下幾種:

  • NEW

    尚未啟動的線程處于此狀态

  • RUNNABLE

    在Java虛拟機中執行的線程處于此狀态

  • BLOCKED

    被阻塞等待螢幕鎖定的線程處于此狀态

  • WAITING

    正在等待另一個線程執行特定動作的線程處于此狀态

  • TIMED WAITING

    正在等待另一個線程執行動作達到指定等待時間的線程處于此狀态

  • TERMINATED

    已退出的線程處于此狀态

一個線程可以在給定時間點處于一個狀态,這些狀态是不反映任何作業系統線程狀态的虛拟機狀态

執行個體如下:

public class TestState {
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}      
  • 線程優先級(priority)
  • Java提供一個線程排程器來監控程式中啟動後進入就緒狀态的所有線程,線程排程器按照優先級決定應該排程哪個線程來執行
  • 線程的優先級用數字表示,範圍從1~10
  • Thread.MIN_PRIORITY = 1;
  • Thread.MAX_PRIORITY = 10;
  • Thread.NORM_PRIORITY = 5;
  • 使用這些方式可以改變(setPriority)或者擷取優先級(getPriority)

    示例如下:

public class TestPriority {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());


        MyPriority myPriority = new MyPriority();
        Thread thread1 = new Thread(myPriority);
        Thread thread2 = new Thread(myPriority);
        Thread thread3 = new Thread(myPriority);
        Thread thread4 = new Thread(myPriority);
        Thread thread5 = new Thread(myPriority);


        thread1.setPriority(1);
        thread1.start();

        thread2.setPriority(4);
        thread2.start();

        thread3.setPriority(8);
        thread3.start();

        thread4.setPriority(9);
        thread4.start();

        thread5.setPriority(10);
        thread5.start();

    }
    
}

class MyPriority implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
    }
}      
  • 守護線程(daemon)
  • 線程分為使用者線程和守護線程;
  • 虛拟機必須確定使用者線程執行完畢;
  • 虛拟機不用等待守護線程執行完畢
  • 守護程序一般用作背景記錄記錄檔、監控記憶體、以及垃圾回收等待

示例如下:

public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        Thread thread = new Thread(god);
        thread.setDaemon(true); //設定線程為守護線程,預設為false
        thread.start();

        //使用者線程
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                System.out.println("你開心的在這個世界上活着"+i);
            }
            System.out.println("=======Goodbye , World!");
        }).start();
    }
}

//守護線程
class God implements Runnable{
    @Override
    public void run() {
        for (;true;){
            System.out.println("上帝保佑着你");
        }
    }
}      

線程同步

若多個線程操作同一個資源,可能會出現線程不安全的情況。

處理多線程問題時,多個線程通路同一個對象,并且某些線程還想修改這個對象,這時候就需要線程同步。線程同步實質上就是一種等待機制,多個需要同時通路此對象的線程進入這個對象的等待池形成隊列,等待前面線程使用完畢,下一個線程再使用。

由于同一個程序的多個線程共享同一塊存儲空間,為了保證資料在方法中被通路時的正确性,在通路時加入鎖機制synchronized,當一個線程獲得對象的排它鎖,獨占資源,其他線程必須等待,使用後釋放鎖即可。該機制會帶來以下問題:

  • 一個線程持有鎖會導緻其他所有需要此鎖的線程挂起;
  • 在多線程競争下,加鎖,釋放鎖會導緻比較多的上下文切換和排程延時,引起性能問題;
  • 如果一個優先級高的線程等待一個優先級低的線程釋放鎖,會導緻優先級倒置,引起性能問題。

同步方法(并發問題一)

  • 由于可以通過private關鍵字來保證資料對象隻能被方法通路,是以隻需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊

    同步方法:​​

    ​public synchronized void method(int args){}​

  • synchronized方法控制對”對象“的通路,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞,方法一旦執行,就獨占該鎖,直到該方法傳回才釋放鎖,後面被阻塞的線程才能獲得這個鎖,并繼續執行(缺陷:若将一個大的方法申明為synchronized,将會影響效率)

假設同時有三個人在買票,票數總共有10張,若不添加同步關鍵字,則會出現線程不安全的問題(即會出現一張票被同時買到的情況)示例如下:

1.不安全情況

public class UnsafeBuyTicket implements Runnable {

    //票數
    private int ticketNums = 10;
    //标志位
    private boolean flag = true;


    @Override
    public void run() {
        //買票
        while (flag) {
            buyTicket();
        }
    }

    public void buyTicket() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }

        //模拟網絡延時
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "張票");
    }


    public static void main(String[] args) {
        UnsafeBuyTicket station = new UnsafeBuyTicket();

        new Thread(station,"苦逼的我").start();
        new Thread(station,"牛逼的你們").start();
        new Thread(station,"可惡的黃牛黨").start();

    }

}      

2.安全情況(加入同步方法)

public class SafeBuyTicket implements Runnable {

    //票數
    private int ticketNums = 10;
    //标志位
    private boolean flag = true;

    @Override
    public void run() {
        //買票
        while (flag) {
            buyTicket();
        }
    }

    //同步方法,關鍵詞synchroinzed.

    //關鍵字是鎖
    //實作的機制是隊列

    //還能所反射的那個class

    public synchronized void buyTicket() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }

        //模拟網絡延時
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "張票");
    }


    public static void main(String[] args) {
        SafeBuyTicket station = new SafeBuyTicket();

        new Thread(station,"苦逼的我").start();
        new Thread(station,"牛逼的你們").start();
        new Thread(station,"可惡的黃牛黨").start();

    }
}      

同步塊(并發問題二)

  • 格式:​

    ​synchronized(Obj obj){}​

    ​,Obj稱之為同步螢幕

    - Obj可以是任何對象,但是推薦使用共享資源作為同步螢幕

    - 同步方法中無需指定同步螢幕,因為同步方法的同步螢幕就是this,就是這個對象本身,或者是class

  • 同步監視的執行過程

    - 第一個線程通路,鎖定同步螢幕,執行其中代碼;

    - 第二個線程通路,發現同步螢幕被鎖定,無法通路;

    - 第一個線程通路完畢,解鎖同步螢幕;

    - 第二個線程通路,發現同步螢幕沒有鎖,然後鎖定并通路

假設同時有兩個人在銀行取錢,若不添加同步關鍵字,則會出現線程不安全的問題(即會出現第一個人把存款取完,在銀行存款還沒有及時重新整理時,第二個人再次取款,則會讓銀行存款變為負數)示例如下:

1.不安全情況

public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"招商卡");

        Bank you = new Bank("痛苦的你",account,50);
        Bank wife = new Bank("開心的媳婦",account,100);

        you.start();
        wife.start();
    }
}


//賬戶
class Account{
    int money;//餘額
    String name; //卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}


//銀行
class Bank extends Thread{
    //存錢:存了多少,取錢:取了多少

    Account account;  //賬戶
    int drawingMoney; //取了多少錢
    int nowMoney; //手裡有多少錢

    public Bank(String name,Account account,int drawingMoney){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {

        //判斷能否取錢
        if (account.money-drawingMoney<0){
            return;
        }

        //為了放大問題發生性,我們加個延時.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //餘額 = 餘額 - 你去走的錢
        account.money = account.money - drawingMoney;
        //你的錢 = 你的錢 + 你取的錢
        nowMoney = drawingMoney + nowMoney;

        System.out.println(this.account.name+"賬戶餘額:"+account.money);
        System.out.println(this.getName()+"手裡的錢:"+nowMoney);


    }
}      

2.安全情況(同步塊)

public class SafeBank {


    public static void main(String[] args) {
        Account2 account = new Account2(100,"招商卡");

        Bank2 you = new Bank2("痛苦的你",account,50);
        Bank2 wife = new Bank2("開心的媳婦",account,100);

        you.start();
        wife.start();
    }
}

//賬戶
//實體類
class Account2{
    int money;//餘額
    String name; //卡名

    public Account2(int money, String name) {
        this.money = money;
        this.name = name;
    }
}


//銀行
class Bank2 extends Thread{
    //存錢:存了多少,取錢:取了多少

    Account2 account;  //賬戶
    int drawingMoney; //取了多少錢
    int nowMoney; //手裡有多少錢

    public Bank2(String name,Account2 account,int drawingMoney){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        drwaing();
    }

    //synchronized本身鎖的是this.就是這個對象本身
    public void drwaing(){

        //提高性能的代碼
        if (account.money<=0){
            return;
        }

        //如何判斷鎖的對象
        // 誰需要實作增删改就去鎖定他
        synchronized (account){

            //判斷能否取錢
            if (account.money-drawingMoney<0){
                System.out.println(Thread.currentThread().getName()+"活該,沒取到錢");
                return;
            }

            //為了放大問題發生性,我們加個延時.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //餘額 = 餘額 - 你去走的錢
            account.money = account.money - drawingMoney;
            //你的錢 = 你的錢 + 你取的錢
            nowMoney = drawingMoney + nowMoney;

            System.out.println(this.account.name+"賬戶餘額:"+account.money);
            System.out.println(this.getName()+"手裡的錢:"+nowMoney);
        }

    }
}      

List(并發問題三)

因為List并非是線程安全的,是以同樣需要用synchronized來令List線程的線程安全。

示例如下:

1.不安全情況

public class UnSafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();

        for (int i = 0; i < 20000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        for (int i = 5;i>0;i--){
            Thread.sleep(1000);
            System.out.println("倒計時"+i);
        }

        System.out.println(list.size());

    }
}      

2.安全情況(同步關鍵字)

public class SafeList {
    public static void main(String[] args) throws InterruptedException {

        List<String> list = new ArrayList<String>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        for (int i = 5;i>0;i--){
            Thread.sleep(1000);
            System.out.println("倒計時"+i);
        }

        System.out.println(list.size());

    }
}
      

線程死鎖

  • 概念:多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運作。而導緻兩個或者多個線程都在等待對方釋放資源,都停止執行的情形。某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能發生“死鎖”的問題
  • 産生死鎖的必要條件:

    - 互斥條件:一個資源每次隻能被一個程序使用

    - 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放

    - 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪

    - 循環等待條件:若幹程序之間形成一種頭尾相連的循環等待資源關系

解決死鎖

上面列出的死鎖的四個必要條件,隻要想辦法破壞其中的任意一個或多個條件,就可以避免死鎖發生

假設有兩個女生,在出門之前同時進行化妝(需要拿鏡子和口紅),若一個女生先拿到鏡子,另一個女生先拿到口紅,之後,為了得到彼此的口紅和鏡子,兩方都需要等待,就會發生死鎖,示例如下:

1.不安全情況

public class DeadLocked {

    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"白雪公主");
        Makeup g2 = new Makeup(1,"灰姑涼");

        new Thread(g1).start();
        new Thread(g2).start();

    }

}

//化妝
class Makeup implements Runnable{

    //選擇
    int choice;
    //誰進來了
    String girlName;

    //兩個對象
    static LipStick lipStick = new LipStick();
    static Mirror mirror = new Mirror();

    public Makeup(int choice,String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        //化妝
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妝的方法
    public void makeup() throws InterruptedException {
        if (choice==0){ //先拿口紅,再拿鏡子
            synchronized (lipStick){
                System.out.println("拿到口紅");
                Thread.sleep(1000);
                //等待拿鏡子的人釋放鎖
                synchronized (mirror){
                    System.out.println("拿到鏡子");
                }
            }

        }else { //先拿鏡子 , 再拿口紅
            synchronized (mirror){
                System.out.println("拿到鏡子");
                Thread.sleep(2000);
                //等待拿口紅的人釋放鎖
                synchronized (lipStick){
                    System.out.println("拿到口紅");
                }
            }
        }

    }

}      

2.安全情況(避免synchronized塊嵌套)

public class DeadLocked {

    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"白雪公主");
        Makeup g2 = new Makeup(1,"灰姑涼");

        new Thread(g1).start();
        new Thread(g2).start();

    }

}

//化妝
class Makeup implements Runnable{

    //選擇
    int choice;
    //誰進來了
    String girlName;

    //兩個對象
    static LipStick lipStick = new LipStick();
    static Mirror mirror = new Mirror();

    public Makeup(int choice,String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        //化妝
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妝的方法
    public void makeup() throws InterruptedException {
        if (choice==0){ //先拿口紅,再拿鏡子
            synchronized (lipStick){
                System.out.println("拿到口紅");
                Thread.sleep(1000);
                //等待拿鏡子的人釋放鎖
            }
            synchronized (mirror){
                System.out.println("拿到鏡子");
            }

        }else { //先拿鏡子 , 再拿口紅
            synchronized (mirror){
                System.out.println("拿到鏡子");
                Thread.sleep(2000);
                //等待拿口紅的人釋放鎖
            }
            synchronized (lipStick){
                System.out.println("拿到口紅");
            }
        }

    }

}





//口紅
class LipStick{

}

//鏡子
class Mirror{

}      

Lock(鎖)

  • 從JDK 5.0開始,Java提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實作同步。同步鎖使用Lock對象充當
  • java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對Lock對象加鎖,線程開始通路共享資源之前應先獲得Lock對象
  • ReentranLock類實作了Lock,它擁有與synchronized相同的并發性和記憶體語義,在實作線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖

示例如下:

public class TestLock {
    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();

        new Thread(helloWorld).start();
        new Thread(helloWorld).start();

    }
}


class HelloWorld implements Runnable{

    int ticketNums = 100;
    //可重入鎖
    private final ReentrantLock lock = new ReentrantLock();

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

            try {
                lock.lock(); //加鎖
                //判斷是否有票
                if (ticketNums>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                }else {
                    break;
                }
            } finally {
                lock.unlock();//解鎖
            }

        }

    }

}      

synchronized 與 Lock 的對比

  • Lock是顯式鎖(手動開啟和關閉鎖,别忘記關閉鎖)synchronized是隐式鎖,出了作用域自動釋放
  • Lock隻有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
  • 使用Lock鎖,JVM将花費較少的時間來排程線程,性能更好。并且具有更好的擴充性(提供更多的子類)
  • 優先使用順序:

    - Lock > 同步代碼塊(已經進入了方法體,配置設定了相應資源) > 同步方法(在方法體之外)

線程協作——生産者消費者模式

  • 應用場景:生産者和消費者問題

    - 假設倉庫隻能存放一件産品,生産者将生産出來的産品放入倉庫,消費者将倉庫中産品取走消費

    - 如果倉庫中沒有産品,則生産者将産品放入倉庫,否則停止生産并等待,直到倉庫中的産品被消費者取走為止

    - 如果倉庫中放有産品,則消費者可以将産品取走消費,否則停止消費并等待,直到倉庫中再次放入産品為止

  • 分析:這是一個線程同步問題,生産者和消費者共享同一個資源,并且生産者和消費者之間互相依賴,互為條件

    - 對于生産者,沒有生産産品之前,要通知消費者等待,而生産了産品之後,又需要馬上通知消費者消費

    - 對于消費者,在消費之後,要通知生産者已經結束消費,需要生産新的産品以供消費

    - 在生産者消費者問題中,僅有synchronized是不夠的:

    1.synchronized可阻止并發更新同一個共享資源,實作了同步

    2.但synchronized不能用來實作不同線程之間的消息傳遞(通信)

  • 線程方法:針對線程之間互相通信的問題,Java提供了幾個方法用于解決

    -​​

    ​wait()​

    ​:表示線程一直等待,直到其他線程通知,與sleep不同,會釋放鎖

    -​​

    ​wait(long timeout)​

    ​​:指定等待的毫秒數

    -​​

    ​notify()​

    ​​:喚醒一個處于等待狀态的線程

    -​​

    ​notifyall()​

    ​​:喚醒同一個對象上所有調用wait()方法的線程,優先級别高的線程優先排程

    注意:以上的均是Object類的方法,都隻能在同步方法或者同步代碼塊中使用,否則會抛出異常IllegaIMonitorStateException

  • 管程法

    - 生産者:負責生産資料的子產品(可能是方法、對象、線程、程序);

    - 消費者:負責處理資料的子產品(可能是方法、對象、線程、程序);

    - 緩沖區:消費者不能直接使用生産者的資料,它們之間有一個緩沖區

    生産者将生産好的資料放入緩沖區,消費者從緩沖區拿出資料

    示例如下:

//思路

//1.思考需要哪些對象?
// 生産 , 消費 , 産品 , 容器

//2.分工

/*
    生産者隻管生産
    消費者隻管消費
    雞: 實體類

    容器 :

    容器添加資料.
    要判斷容器是否滿 , 滿了等待消費者消費
    沒有滿,通知生産者生産

    容器減少資料
    判斷還有沒有資料, 沒有資料的話 . 等待生産者生産
    消費完畢 , 通知生産者生産
 */


import java.sql.SQLOutput;

//測試生産者和消費者問題
public class TestPC {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();

        new Productor(synContainer).start();
        new Consumer(synContainer).start();
    }
}


//生産者
class Productor extends Thread{
    //需要向容器中加入産品
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
        }
        @Override
        public void run() {
            for (int i = 1; i < 100; i++) {
                //生産者添加産品
            container.push(new Chicken(i));
            System.out.println("生産者生産了"+i+"雞");
        }
    }
}

//消費者
class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            //消費者拿走産品
            Chicken chicken = container.pop();
            System.out.println("消費者消費了"+chicken.id+"雞");
        }
    }
}


//緩沖區-->容器
class SynContainer{

    //容器
    Chicken[] chickens = new Chicken[10];



    //容器的計數器
    int num = 0;

    //生産者放入産品
    public synchronized void push(Chicken chicken) {

        //假如容易已經滿了,就不用放,等待消費者消費
        if (num>=chickens.length){

            //等待消費者消費
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        //假如容器沒有滿 , 通知生産生成

        System.out.println("num,,,,,"+num);
        chickens[num] = chicken;
        System.out.println("數組有多少個元素"+num);
        num++;
        //通知消費者消費
        this.notifyAll();

    }

    //消費者拿走産品
    public synchronized Chicken pop(){
        //假如容器空的,等待
        if (num<=0){
            //等待生産
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        num--;
        Chicken chicken = chickens[num];
        //通知生産者生産
        this.notifyAll();
        return chicken;
    }


}

//産品->雞
class Chicken {
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}      
  • 信号燈法

    在産品中設定一個标志位,利用标志位(flag)來選擇讓生産者執行邏輯還是讓消費者執行邏輯(類似于紅燈停,綠燈行)

    示例如下:

//生産者消費2
//生産者--->演員
//消費者--->觀衆
//産品:信号燈--->電視----->聲音

public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();

        new Player(tv).start();
        new Watcher(tv).start();
    }
}


//生産者
class Player extends Thread{
    TV tv;

    public Player(TV tv){
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i%2==0){
                this.tv.play("節目:快樂大學營播放中");
                System.out.println();
            }else {
                this.tv.play("廣告:抖音,記錄美好生活");
            }
        }
    }
}

//消費者
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv){
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//電視
class TV{
    //演員說話 , 觀衆等待
    //觀衆觀看 , 演員等待
    boolean flag = true;

    //說話
    String voice;

    //表演
    public synchronized void play(String voice){

        //演員等待
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("表演了"+voice);
        this.voice = voice;

        //讓觀衆觀看
        this.notifyAll();
        this.flag = !this.flag;

    }


    //觀看
    public synchronized void watch(){


        //觀衆等待
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("觀衆聽到了: "+voice);

        //通知演員說話
        this.notifyAll();

        this.flag = !this.flag;
    }

}      

線程池

  • 背景:經常建立和銷毀、使用量特别大的資源,比如并發情況下的線程,對性能影響很大
  • 思路:提前建立好多個線程,放入線程池中,使用時直接擷取,使用完放回池中。可以避免頻繁建立銷毀、實作重複利用。類似生活中的公共交通工具
  • 好處:

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

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

    - 便于線程管理

  • 定義:

    - corePoolSize:核心池的大小

    - maximumPoolSize:最大線程數

    - keepAliveTime:線程沒有任務時最多保持多長時間後終止

  • 使用線程池:JDK 5.0起提供了線程池相關API:ExecutorService和Executors

    - ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor

    - void execute(Runnable command):執行任務/指令,沒有傳回值,一般用來執行Runnable

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

    - void shutdown():關閉線程池

    - Executors:工具類、線程池的工廠類,用于建立并傳回不同類型的線程池

public class ThreadPool{

    public static void main(String[] args) {

        //建立一個線程池(池子大小)
        ExecutorService pool = Executors.newFixedThreadPool(10);

        //執行runnable接口實作類
        pool.execute(new MyThread4());
        pool.execute(new MyThread4());
        pool.execute(new MyThread4());
        pool.execute(new MyThread4());

        //關閉連接配接池
        pool.shutdown();

    }

}

class MyThread4 implements Runnable{
    @Override
    public void run() {
        System.out.println("MyThread4");
    }
}