1. 程式人生 > 其它 >【重學Java】多執行緒基礎(三種建立方式,執行緒安全,生產者消費者)

【重學Java】多執行緒基礎(三種建立方式,執行緒安全,生產者消費者)

實現多執行緒

簡單瞭解多執行緒【理解】

是指從軟體或者硬體上實現多個執行緒併發執行的技術。
具有多執行緒能力的計算機因有硬體支援而能夠在同一時間執行多個執行緒,提升效能。

併發和並行【理解】

  • 並行:在同一時刻,有多個指令在多個CPU上同時執行。
  • 併發:在同一時刻,有多個指令在單個CPU上交替執行。

程序和執行緒【理解】

  • 程序:是正在執行的程式

    獨立性:程序是一個能獨立執行的基本單位,同時也是系統分配資源和排程的獨立單位
    動態性:程序的實質是程式的一次執行過程,程序是動態產生,動態消亡的
    併發性:任何程序都可以同其他程序一起併發執行

  • 執行緒:是程序中的單個順序控制流,是一條執行路徑,是一個程序中的執行場景/執行單元。

    ​ 單執行緒:一個程序如果只有一條執行路徑,則稱為單執行緒程式

    ​ 多執行緒:一個程序如果有多條執行路徑,則稱為多執行緒程式

實現多執行緒方式一:繼承Thread類【應用】

  • 方法介紹

    方法名 說明
    void run() 線上程開啟後,此方法將被呼叫執行
    void start() 使此執行緒開始執行,Java虛擬機器會呼叫run方法()
  • 實現步驟

    • 定義一個類MyThread繼承Thread類
    • 在MyThread類中重寫run()方法
    • 建立MyThread類的物件
    • 啟動執行緒
  • 程式碼演示

    public class MyThread extends Thread {
        @Override
        public void run() {
            for(int i=0; i<100; i++) {
                System.out.println(i);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
    
    //        my1.run();
    //        my2.run();
    
            //void start() 導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法
            my1.start();
            my2.start();
        }
    }
    
  • 兩個小問題

    • 為什麼要重寫run()方法?

      因為run()是用來封裝被執行緒執行的程式碼

    • run()方法和start()方法的區別?

      run():封裝執行緒執行的程式碼,直接呼叫,相當於普通方法的呼叫,並沒有開啟執行緒

      start():啟動執行緒;然後由JVM呼叫此執行緒的run()方法

實現多執行緒方式二:實現Runnable介面【應用】

  • Thread構造方法

    方法名 說明
    Thread(Runnable target) 分配一個新的Thread物件
    Thread(Runnable target, String name) 分配一個新的Thread物件
  • 實現步驟

    • 定義一個類MyRunnable實現Runnable介面
    • 在MyRunnable類中重寫run()方法
    • 建立MyRunnable類的物件
    • 建立Thread類的物件,把MyRunnable物件作為構造方法的引數
    • 啟動執行緒
  • 程式碼演示

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for(int i=0; i<100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
    public class MyRunnableDemo {
        public static void main(String[] args) {
            //建立MyRunnable類的物件
            MyRunnable my = new MyRunnable();
    
            //建立Thread類的物件,把MyRunnable物件作為構造方法的引數
            //Thread(Runnable target)
    //        Thread t1 = new Thread(my);
    //        Thread t2 = new Thread(my);
            //Thread(Runnable target, String name)
            Thread t1 = new Thread(my,"坦克");
            Thread t2 = new Thread(my,"飛機");
    
            //啟動執行緒
            t1.start();
            t2.start();
        }
    }
    

實現多執行緒方式三: 實現Callable介面【應用】

  • 方法介紹

    方法名 說明
    V call() 計算結果,如果無法計算結果,則丟擲一個異常
    FutureTask(Callable callable) 建立一個 FutureTask,一旦執行就執行給定的 Callable
    V get() 如有必要,等待計算完成,然後獲取其結果
  • 理解:FutureTask 想一個中間類,可以利用它實現介面,建立Thread執行緒,也可以利用它獲取執行緒返回值

  • 實現步驟

    • 定義一個類MyCallable實現Callable介面
    • 在MyCallable類中重寫call()方法
    • 建立MyCallable類的物件
    • 建立Future的實現類FutureTask物件,把MyCallable物件作為構造方法的引數
    • 建立Thread類的物件,把FutureTask物件作為構造方法的引數
    • 啟動執行緒
    • 再呼叫get方法,就可以獲取執行緒結束之後的結果。
  • 程式碼演示

    public class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            for (int i = 0; i < 100; i++) {
                System.out.println("跟女孩表白" + i);
            }
            //返回值就表示執行緒執行完畢之後的結果
            return "答應";
        }
    }
    public class Demo {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //執行緒開啟之後需要執行裡面的call方法
            MyCallable mc = new MyCallable();
    
            //Thread t1 = new Thread(mc);
    
            //可以獲取執行緒執行完畢之後的結果.也可以作為引數傳遞給Thread物件
            FutureTask<String> ft = new FutureTask<>(mc);
    
            //建立執行緒物件
            Thread t1 = new Thread(ft);
    
            String s = ft.get();
            //開啟執行緒
            t1.start();
    
            //String s = ft.get();
            System.out.println(s);
        }
    }
    
  • 三種實現方式的對比

    • 實現Runnable、Callable介面
      • 好處: 擴充套件性強,實現該介面的同時還可以繼承其他的類
      • 缺點: 程式設計相對複雜,不能直接使用Thread類中的方法
    • 繼承Thread類
      • 好處: 程式設計比較簡單,可以直接使用Thread類中的方法
      • 缺點: 可以擴充套件性較差,不能再繼承其他的類

設定和獲取執行緒名稱【應用】

  • 方法介紹

    方法名 說明
    void setName(String name) 將此執行緒的名稱更改為等於引數name
    String getName() 返回此執行緒的名稱
    Thread currentThread() 返回對當前正在執行的執行緒物件的引用
  • 程式碼演示

    public class MyThread extends Thread {
        public MyThread() {}
        public MyThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName()+":"+i);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
            //執行緒有預設名字,格式:thread-編號
            //void setName(String name):將此執行緒的名稱更改為等於引數 name
            my1.setName("高鐵");
            my2.setName("飛機");
    
            //Thread(String name)
            MyThread my1 = new MyThread("高鐵");
            MyThread my2 = new MyThread("飛機");
            //構造方法也可以給執行緒設定名字,但是要寫出執行緒的無參和有參建構函式
            /*class MyThread extends Thread{
              public MyThread(String name) {
                 super(name);
              }
              public MyThread() {
                @Override
                public void run(){
                 //執行的程式碼
                }
            }
      	  */
            my1.start();
            my2.start();
    
            //static Thread currentThread() 返回對當前正在執行的執行緒物件的引用
            System.out.println(Thread.currentThread().getName());//MyThread類內沒有getName()方法
        }
    }
    

執行緒休眠【應用】

  • 相關方法

    方法名 說明
    static void sleep(long millis) 使當前正在執行的執行緒停留(暫停執行)指定的毫秒數
  • 程式碼演示

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
        }
    }
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            /*System.out.println("睡覺前");
            Thread.sleep(3000);
            System.out.println("睡醒了");*/
    
            MyRunnable mr = new MyRunnable();
    
            Thread t1 = new Thread(mr);
            Thread t2 = new Thread(mr);
    
            t1.start();
            t2.start();
        }
    }
    

執行緒優先順序【應用】

  • 執行緒排程

    • 兩種排程方式

      • 分時排程模型:所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間片
      • 搶佔式排程模型:優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個,優先順序高的執行緒獲取的 CPU 時間片相對多一些
    • Java使用的是搶佔式排程模型

    • 隨機性

      假如計算機只有一個 CPU,那麼 CPU 在某一個時刻只能執行一條指令,執行緒只有得到CPU時間片,也就是使用權,才可以執行指令。所以說多執行緒程式的執行是有隨機性,因為誰搶到CPU的使用權是不一定的

  • 優先順序相關方法

    方法名 說明
    final int getPriority() 返回此執行緒的優先順序
    final void setPriority(int newPriority) 更改此執行緒的優先順序執行緒預設優先順序是5;執行緒優先順序的範圍是:1-10
  • 注意
    執行緒優先順序的範圍是1~10
    執行緒預設優先順序是5
    優先順序只是提高執行緒搶佔CPU執行權的機率,並不一定提高實際的搶佔率

  • 程式碼演示

    public class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
            return "執行緒執行完畢了";
        }
    }
    public class Demo {
        public static void main(String[] args) {
            //優先順序: 1 - 10 預設值:5
            MyCallable mc = new MyCallable();
    
            FutureTask<String> ft = new FutureTask<>(mc);
    
            Thread t1 = new Thread(ft);
            t1.setName("飛機");
            t1.setPriority(10);
            //System.out.println(t1.getPriority());//5
            t1.start();
    
            MyCallable mc2 = new MyCallable();
    
            FutureTask<String> ft2 = new FutureTask<>(mc2);
    
            Thread t2 = new Thread(ft2);
            t2.setName("坦克");
            t2.setPriority(1);
            //System.out.println(t2.getPriority());//5
            t2.start();
        }
    }
    

守護執行緒【應用】

  • 相關方法

    方法名 說明
    void setDaemon(boolean on) 將此執行緒標記為守護執行緒,當執行的執行緒都是守護執行緒時,Java虛擬機器將退出
  • 程式碼演示

    public class MyThread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(getName() + "---" + i);
            }
        }
    }
    public class MyThread2 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + "---" + i);
            }
        }
    }
    public class Demo {
        public static void main(String[] args) {
            MyThread1 t1 = new MyThread1();
            MyThread2 t2 = new MyThread2();
    
            t1.setName("女神");
            t2.setName("備胎");
    
            //把第二個執行緒設定為守護執行緒
            //當普通執行緒執行完之後,那麼守護執行緒也沒有繼續執行下去的必要了.
            t2.setDaemon(true);
    
            t1.start();
            t2.start();
        }
    }
    

執行緒同步

賣票【應用】

  • 案例需求

    某電影院目前正在上映國產大片,共有100張票,而它有3個視窗賣票,請設計一個程式模擬該電影院賣票

  • 實現步驟

    • 定義一個類SellTicket實現Runnable介面,裡面定義一個成員變數:private int tickets = 100;

    • 在SellTicket類中重寫run()方法實現賣票,程式碼步驟如下

    • 判斷票數大於0,就賣票,並告知是哪個視窗賣的

    • 賣了票之後,總票數要減1

    • 票賣沒了,執行緒停止

    • 定義一個測試類SellTicketDemo,裡面有main方法,程式碼步驟如下

    • 建立SellTicket類的物件

    • 建立三個Thread類的物件,把SellTicket物件作為構造方法的引數,並給出對應的視窗名稱

    • 啟動執行緒

  • 程式碼實現

    public class SellTicket implements Runnable {
        private int tickets = 100;
        //在SellTicket類中重寫run()方法實現賣票,程式碼步驟如下
        @Override
        public void run() {
            while (true) {
                if(ticket <= 0){
                        //賣完了
                        break;
                    }else{
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticket--;
                        System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticket + "張票");
                    }
            }
        }
    }
    public class SellTicketDemo {
        public static void main(String[] args) {
            //建立SellTicket類的物件
            SellTicket st = new SellTicket();
    
            //建立三個Thread類的物件,把SellTicket物件作為構造方法的引數,並給出對應的視窗名稱
            Thread t1 = new Thread(st,"視窗1");
            Thread t2 = new Thread(st,"視窗2");
            Thread t3 = new Thread(st,"視窗3");
    
            //啟動執行緒
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

賣票案例的問題【理解】

  • 賣票出現了問題

    • 相同的票出現了多次

    • 出現了負數的票

  • 問題產生原因

    執行緒執行的隨機性導致的,可能在賣票過程中丟失cpu的執行權,導致出現問題

同步程式碼塊解決資料安全問題【應用】

  • 安全問題出現的條件

    • 是多執行緒環境

    • 有共享資料

    • 有多條語句操作共享資料

  • 如何解決多執行緒安全問題呢?

    • 基本思想:讓程式沒有安全問題的環境
  • 怎麼實現呢?

    • 把多條語句操作共享資料的程式碼給鎖起來,讓任意時刻只能有一個執行緒執行即可

    • Java提供了同步程式碼塊的方式來解決

  • 同步程式碼塊格式:

    synchronized(任意物件) { 
    	多條語句操作共享資料的程式碼 
    }
    
  • 注意同步程式碼塊鎖住的物件必須是多個執行緒共享的物件,否則無效。
    synchronized(任意物件):就相當於給程式碼加鎖了,任意物件就可以看成是一把鎖

    public class test33 {
      public static void main(String[] args) {
          MyThread thread1 = new MyThread();
          MyThread thread2 = new MyThread();
          MyThread thread3 = new MyThread();
          thread1.setName("執行緒1");
          thread2.setName("執行緒2");
          thread3.setName("執行緒3");
          thread1.start();
          thread2.start();
          thread3.start();
      }
    }
    
    class MyThread extends Thread {
      private static int ticket = 100;
      public  Object obj = new Object();
    
      @Override
      public void run() {
    
          while (true) {
              synchronized (obj) {
                  if (ticket <= 0) break;
                  else {
                      try {
                          Thread.sleep(10);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      ticket--;
                      System.out.println(Thread.currentThread().getName() + "執行了,還剩下" + ticket + "張票");
    
                  }
              }
          }
    
      }
    }
    

    該案例中建立的三個執行緒,有三個不同的Object成員物件,不共享,相當於沒加鎖,無效。
    需要改成 public static Object obj = new Object(); 讓obj成為共享的成員物件。
    同樣,該案例中如果鎖為this及synchronized(this)則依然無效,因為this所指的物件不是共享的。

  • 同步的好處和弊端

    • 好處:解決了多執行緒的資料安全問題

    • 弊端:當執行緒很多時,因為每個執行緒都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的執行效率

  • 程式碼演示

    public class SellTicket implements Runnable {
        private int tickets = 100;
        private Object obj = new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized (obj) { // 對可能有安全問題的程式碼加鎖,多個執行緒必須使用同一把鎖
                    //t1進來後,就會把這段程式碼給鎖起來
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                            //t1休息100毫秒
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //視窗1正在出售第100張票
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");
                        tickets--; //tickets = 99;
                    }
                }
                //t1出來了,這段程式碼的鎖就被釋放了
            }
        }
    }
    
    public class SellTicketDemo {
        public static void main(String[] args) {
            SellTicket st = new SellTicket();
    
            Thread t1 = new Thread(st, "視窗1");
            Thread t2 = new Thread(st, "視窗2");
            Thread t3 = new Thread(st, "視窗3");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

同步方法解決資料安全問題【應用】

  • 同步方法的格式

    同步方法:就是把synchronized關鍵字加到方法上

    修飾符 synchronized 返回值型別 方法名(方法引數) { 
    	方法體;
    }
    

    同步方法的鎖物件是什麼呢?

    ​ this

  • 靜態同步方法

    同步靜態方法:就是把synchronized關鍵字加到靜態方法上

    修飾符 static synchronized 返回值型別 方法名(方法引數) { 
    	方法體;
    }
    

    同步靜態方法的鎖物件是什麼呢?

    ​ 類名.class

  • 程式碼演示

    public class MyRunnable implements Runnable {
        private static int ticketCount = 100;
    
        @Override
        public void run() {
            while(true){
                if("視窗一".equals(Thread.currentThread().getName())){
                    //同步方法
                    boolean result = synchronizedMthod();
                    if(result){
                        break;
                    }
                }
    
                if("視窗二".equals(Thread.currentThread().getName())){
                    //同步程式碼塊
                    synchronized (MyRunnable.class){
                        if(ticketCount == 0){
                           break;
                        }else{
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            ticketCount--;
                            System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票");
                        }
                    }
                }
    
            }
        }
    
        private static synchronized boolean synchronizedMthod() {
            if(ticketCount == 0){
                return true;
            }else{
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticketCount--;
                System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票");
                return false;
            }
        }
    }
    

    測試類

    public class Demo {
        public static void main(String[] args) {
            MyRunnable mr = new MyRunnable();
    
            Thread t1 = new Thread(mr);
            Thread t2 = new Thread(mr);
      
            t1.setName("視窗一");
            t2.setName("視窗二");
      
            t1.start();
            t2.start();
        }
    
    }
    
    

Lock鎖【應用】

雖然我們可以理解同步程式碼塊和同步方法的鎖物件問題,但是我們並沒有直接看到在哪裡加上了鎖,在哪裡釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以後提供了一個新的鎖物件Lock

Lock是介面不能直接例項化,這裡採用它的實現類ReentrantLock來例項化。預設鎖住當前物件

  • ReentrantLock構造方法

    方法名 說明
    ReentrantLock() 建立一個ReentrantLock的例項
  • 加鎖解鎖方法

    方法名 說明
    void lock() 獲得鎖
    void unlock() 釋放鎖
  • 程式碼演示

    public class Ticket implements Runnable {
        //票的數量
        private int ticket = 100;
        private Object obj = new Object();
        private ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                //synchronized (obj){//多個執行緒必須使用同一把鎖.
                try {
                    lock.lock();
                    if (ticket <= 0) {
                        //賣完了
                        break;
                    } else {
                        Thread.sleep(100);
                        ticket--;
                        System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticket + "張票");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                // }
            }
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            Thread t1 = new Thread(ticket);
            Thread t2 = new Thread(ticket);
            Thread t3 = new Thread(ticket);
    
            t1.setName("視窗一");
            t2.setName("視窗二");
            t3.setName("視窗三");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

死鎖【理解】

  • 概述

    執行緒死鎖是指由於兩個或者多個執行緒互相持有對方所需要的資源,導致這些執行緒處於等待狀態,無法前往執行

  • 什麼情況下會產生死鎖

    1. 資源有限
    2. 同步巢狀(鎖的巢狀)
  • 程式碼演示

    public class Demo {
        public static void main(String[] args) {
            Object objA = new Object();
            Object objB = new Object();
    
            new Thread(()->{
                while(true){
                    synchronized (objA){
                        //執行緒一
      				  try {
                                Thread.sleep(100); //休眠為了使執行緒二獲得鎖objB
                            } catch (InterruptedException e) {
                                 e.printStackTrace();
                              }
                        synchronized (objB){
                            System.out.println("小康同學正在走路");
                        }
                    }
                }
            }).start();
    
            new Thread(()->{
                while(true){
                    synchronized (objB){
                        //執行緒二
      				  try {
                                Thread.sleep(100); //休眠為了使執行緒一獲得鎖objA
                            } catch (InterruptedException e) {
                                 e.printStackTrace();
                              }
                        synchronized (objA){
                            System.out.println("小薇同學正在走路");
                        }
                    }
                }
            }).start();
        }
    }
    

生產者消費者

生產者和消費者模式概述【應用】

  • 概述

    生產者消費者模式是一個十分經典的多執行緒協作的模式,弄懂生產者消費者問題能夠讓我們對多執行緒程式設計的理解更加深刻。

    所謂生產者消費者問題,實際上主要是包含了兩類執行緒:

    ​ 一類是生產者執行緒用於生產資料

    ​ 一類是消費者執行緒用於消費資料

    為了解耦生產者和消費者的關係,通常會採用共享的資料區域,就像是一個倉庫

    生產者生產資料之後直接放置在共享資料區中,並不需要關心消費者的行為

    消費者只需要從共享資料區中去獲取資料,並不需要關心生產者的行為

  • Object類的等待和喚醒方法

    方法名 說明
    void wait() 導致當前執行緒等待,直到另一個執行緒呼叫該物件的 notify()方法或 notifyAll()方法
    void notify() 喚醒正在等待物件監視器的單個執行緒
    void notifyAll() 喚醒正在等待物件監視器的所有執行緒

生產者和消費者案例【應用】

  • 案例需求

    • 桌子類(Desk):定義表示包子數量的變數,定義鎖物件變數,定義標記桌子上有無包子的變數

    • 生產者類(Cooker):實現Runnable介面,重寫run()方法,設定執行緒任務

      1.判斷是否有包子,決定當前執行緒是否執行

      2.如果有包子,就進入等待狀態,如果沒有包子,繼續執行,生產包子

      3.生產包子之後,更新桌子上包子狀態,喚醒消費者消費包子

    • 消費者類(Foodie):實現Runnable介面,重寫run()方法,設定執行緒任務

      1.判斷是否有包子,決定當前執行緒是否執行

      2.如果沒有包子,就進入等待狀態,如果有包子,就消費包子

      3.消費包子後,更新桌子上包子狀態,喚醒生產者生產包子

    • 測試類(Demo):裡面有main方法,main方法中的程式碼步驟如下

      建立生產者執行緒和消費者執行緒物件

      分別開啟兩個執行緒

  • 程式碼實現

    public class Desk {
    
        //定義一個標記
        //true 就表示桌子上有漢堡包的,此時允許吃貨執行
        //false 就表示桌子上沒有漢堡包的,此時允許廚師執行
        public static boolean flag = false;
    
        //漢堡包的總數量
        public static int count = 10;
    
        //鎖物件
        public static final Object lock = new Object();
    }
    
    public class Cooker extends Thread {
    //    生產者步驟:
    //            1,判斷桌子上是否有漢堡包
    //    如果有就等待,如果沒有才生產。
    //            2,把漢堡包放在桌子上。
    //            3,叫醒等待的消費者開吃。
        @Override
        public void run() {
            while(true){
                synchronized (Desk.lock){
                    if(Desk.count == 0){
                        break;
                    }else{
                        if(!Desk.flag){
                            //生產
                            System.out.println("廚師正在生產漢堡包");
                            Desk.flag = true;
                            Desk.lock.notifyAll();
                        }else{
                            try {
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    
    public class Foodie extends Thread {
        @Override
        public void run() {
    //        1,判斷桌子上是否有漢堡包。
    //        2,如果沒有就等待。
    //        3,如果有就開吃
    //        4,吃完之後,桌子上的漢堡包就沒有了
    //                叫醒等待的生產者繼續生產
    //        漢堡包的總數量減一
    
            //套路:
                //1. while(true)死迴圈
                //2. synchronized 鎖,鎖物件要唯一
                //3. 判斷,共享資料是否結束. 結束
                //4. 判斷,共享資料是否結束. 沒有結束
            while(true){
                synchronized (Desk.lock){
                    if(Desk.count == 0){
                        break;
                    }else{
                        if(Desk.flag){
                            //有
                            System.out.println("吃貨在吃漢堡包");
                            Desk.flag = false;
                            Desk.lock.notifyAll();
                            Desk.count--;
                        }else{
                            //沒有就等待
                            //使用什麼物件當做鎖,那麼就必須用這個物件去呼叫等待和喚醒的方法.
                            try {
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
    
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            /*消費者步驟:
            1,判斷桌子上是否有漢堡包。
            2,如果沒有就等待。
            3,如果有就開吃
            4,吃完之後,桌子上的漢堡包就沒有了
                    叫醒等待的生產者繼續生產
            漢堡包的總數量減一*/
    
            /*生產者步驟:
            1,判斷桌子上是否有漢堡包
            如果有就等待,如果沒有才生產。
            2,把漢堡包放在桌子上。
            3,叫醒等待的消費者開吃。*/
    
            Foodie f = new Foodie();
            Cooker c = new Cooker();
    
            f.start();
            c.start();
    
        }
    }
    

生產者和消費者案例優化【應用】

  • 需求

    • 將Desk類中的變數,採用面向物件的方式封裝起來
    • 生產者和消費者類中構造方法接收Desk類物件,之後在run方法中進行使用
    • 建立生產者和消費者執行緒物件,構造方法中傳入Desk類物件
    • 開啟兩個執行緒
  • 程式碼實現

    public class Desk {
    
        //定義一個標記
        //true 就表示桌子上有漢堡包的,此時允許吃貨執行
        //false 就表示桌子上沒有漢堡包的,此時允許廚師執行
        //public static boolean flag = false;
        private boolean flag;
    
        //漢堡包的總數量
        //public static int count = 10;
        //以後我們在使用這種必須有預設值的變數
       // private int count = 10;
        private int count;
    
        //鎖物件
        //public static final Object lock = new Object();
        private final Object lock = new Object();
    
        public Desk() {
            this(false,10); // 在空參內部呼叫帶參,對成員變數進行賦值,之後就可以直接使用成員變量了
        }
    
        public Desk(boolean flag, int count) {
            this.flag = flag;
            this.count = count;
        }
    
        public boolean isFlag() {
            return flag;
        }
    
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    
        public int getCount() {
            return count;
        }
    
        public void setCount(int count) {
            this.count = count;
        }
    
        public Object getLock() {
            return lock;
        }
    
        @Override
        public String toString() {
            return "Desk{" +
                    "flag=" + flag +
                    ", count=" + count +
                    ", lock=" + lock +
                    '}';
        }
    }
    
    public class Cooker extends Thread {
    
        private Desk desk;
    
        public Cooker(Desk desk) {
            this.desk = desk;
        }
    //    生產者步驟:
    //            1,判斷桌子上是否有漢堡包
    //    如果有就等待,如果沒有才生產。
    //            2,把漢堡包放在桌子上。
    //            3,叫醒等待的消費者開吃。
    
        @Override
        public void run() {
            while(true){
                synchronized (desk.getLock()){
                    if(desk.getCount() == 0){
                        break;
                    }else{
                        //System.out.println("驗證一下是否執行了");
                        if(!desk.isFlag()){
                            //生產
                            System.out.println("廚師正在生產漢堡包");
                            desk.setFlag(true);
                            desk.getLock().notifyAll();
                        }else{
                            try {
                                desk.getLock().wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    
    public class Foodie extends Thread {
        private Desk desk;
    
        public Foodie(Desk desk) {
            this.desk = desk;
        }
    
        @Override
        public void run() {
    //        1,判斷桌子上是否有漢堡包。
    //        2,如果沒有就等待。
    //        3,如果有就開吃
    //        4,吃完之後,桌子上的漢堡包就沒有了
    //                叫醒等待的生產者繼續生產
    //        漢堡包的總數量減一
    
            //套路:
                //1. while(true)死迴圈
                //2. synchronized 鎖,鎖物件要唯一
                //3. 判斷,共享資料是否結束. 結束
                //4. 判斷,共享資料是否結束. 沒有結束
            while(true){
                synchronized (desk.getLock()){
                    if(desk.getCount() == 0){
                        break;
                    }else{
                        //System.out.println("驗證一下是否執行了");
                        if(desk.isFlag()){
                            //有
                            System.out.println("吃貨在吃漢堡包");
                            desk.setFlag(false);
                            desk.getLock().notifyAll();
                            desk.setCount(desk.getCount() - 1);
                        }else{
                            //沒有就等待
                            //使用什麼物件當做鎖,那麼就必須用這個物件去呼叫等待和喚醒的方法.
                            try {
                                desk.getLock().wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
    
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            /*消費者步驟:
            1,判斷桌子上是否有漢堡包。
            2,如果沒有就等待。
            3,如果有就開吃
            4,吃完之後,桌子上的漢堡包就沒有了
                    叫醒等待的生產者繼續生產
            漢堡包的總數量減一*/
    
            /*生產者步驟:
            1,判斷桌子上是否有漢堡包
            如果有就等待,如果沒有才生產。
            2,把漢堡包放在桌子上。
            3,叫醒等待的消費者開吃。*/
    
            Desk desk = new Desk();
    
            Foodie f = new Foodie(desk);
            Cooker c = new Cooker(desk);
    
            f.start();
            c.start();
    
        }
    }
    

阻塞佇列基本使用【理解】

  • 阻塞佇列繼承結構
  • 常見BlockingQueue:

    ArrayBlockingQueue: 底層是陣列,有界

    LinkedBlockingQueue: 底層是連結串列,無界.但不是真正的無界,最大為int的最大值

  • BlockingQueue的核心方法:

    put(anObject): 將引數放入佇列,如果放不進去會阻塞

    take(): 取出第一個資料,取不到會阻塞

  • 程式碼示例

    public class Demo02 {
        public static void main(String[] args) throws Exception {
            // 建立阻塞佇列的物件,容量為 1
            ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
    
            // 儲存元素
            arrayBlockingQueue.put("漢堡包");
    
            // 取元素
            System.out.println(arrayBlockingQueue.take());
            System.out.println(arrayBlockingQueue.take()); // 取不到會阻塞
    
            System.out.println("程式結束了");
        }
    }
    

阻塞佇列實現等待喚醒機制【理解】

  • 案例需求

    • 生產者類(Cooker):實現Runnable介面,重寫run()方法,設定執行緒任務

      1.構造方法中接收一個阻塞佇列物件

      2.在run方法中迴圈向阻塞佇列中新增包子

      3.列印新增結果

    • 消費者類(Foodie):實現Runnable介面,重寫run()方法,設定執行緒任務

      1.構造方法中接收一個阻塞佇列物件

      2.在run方法中迴圈獲取阻塞佇列中的包子

      3.列印獲取結果

    • 測試類(Demo):裡面有main方法,main方法中的程式碼步驟如下

      建立阻塞佇列物件

      建立生產者執行緒和消費者執行緒物件,構造方法中傳入阻塞佇列物件

      分別開啟兩個執行緒

  • 程式碼實現

    public class Cooker extends Thread {
    
        private ArrayBlockingQueue<String> bd;
    
        public Cooker(ArrayBlockingQueue<String> bd) {
            this.bd = bd;
        }
    //    生產者步驟:
    //            1,判斷桌子上是否有漢堡包
    //    如果有就等待,如果沒有才生產。
    //            2,把漢堡包放在桌子上。
    //            3,叫醒等待的消費者開吃。
    
        @Override
        public void run() {
            while (true) {
                try {
                    bd.put("漢堡包");
                    System.out.println("廚師放入一個漢堡包");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Foodie extends Thread {
        private ArrayBlockingQueue<String> bd;
    
        public Foodie(ArrayBlockingQueue<String> bd) {
            this.bd = bd;
        }
    
        @Override
        public void run() {
    //        1,判斷桌子上是否有漢堡包。
    //        2,如果沒有就等待。
    //        3,如果有就開吃
    //        4,吃完之後,桌子上的漢堡包就沒有了
    //                叫醒等待的生產者繼續生產
    //        漢堡包的總數量減一
    
            //套路:
            //1. while(true)死迴圈
            //2. synchronized 鎖,鎖物件要唯一
            //3. 判斷,共享資料是否結束. 結束
            //4. 判斷,共享資料是否結束. 沒有結束
            while (true) {
                try {
                    String take = bd.take();
                    System.out.println("吃貨將" + take + "拿出來吃了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
    
            Foodie f = new Foodie(bd);
            Cooker c = new Cooker(bd);
    
            f.start();
            c.start();
        }
    }
    

注意:最終的列印結果可能回出現同樣的語句重複輸出,這不符合預期。
這是因為ArrayBlockingQueue的底層原始碼是用ReentrantLock鎖了,實現執行緒同步,但是System.out語句是我們自己寫的,列印語句沒有實現執行緒同步,所以可能列印的時候出現問題,這只是個小問題。