【重學Java】多線程基礎(三種創建方式,線程安全,生產者消費者)


實現多線程

簡單了解多線程【理解】

是指從軟件或者硬件上實現多個線程並發執行的技術。
具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多個線程,提升性能。

image

並發和並行【理解】

  • 並行:在同一時刻,有多個指令在多個CPU上同時執行。

image

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

image

進程和線程【理解】

  • 進程:是正在運行的程序

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

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

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

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

image

實現多線程方式一:繼承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線程,也可以利用它獲取線程返回值

  • 實現Callable接口
    這種方式的優點:可以獲取到線程的執行結果。
    這種方式的缺點:效率比較低,在獲取t線程執行結果的時候,當前線程受阻塞,效率較低。
    java.util.concurrent.FutureTask; JUC包下的,屬於java的並發包,老JDK中沒有這個包。新特性。

  • 實現步驟

    • 定義一個類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());
      }
  }

線程休眠【應用】

  • 相關方法

    方法名 說明
    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的使用權是不一定的

image

  • 優先級相關方法

    方法名 說明
    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();
      }
  }

線程合並【應用】

方法名 說明
public final void join() 使某線程合並到當前線程中,當前線程受阻塞,某線程執行直到結束。
public static void main(String[] args) {
        System.out.println("main begin");

        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();

        //合並線程
        try {
            t.join(); // t合並到當前線程中,當前線程受阻塞,t線程執行直到結束。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main over");
}

線程讓位【應用】

方法名 說明
static void yield() 暫停當前正在執行的線程對象,並執行其他線程
  • yield()方法不是阻塞方法。讓當前線程讓位,讓給其它線程使用。
  • yield()方法的執行會讓當前線程從“運行狀態”回到“就緒狀態”。
  • 注意:在回到就緒之后,有可能還會再次搶到。
/*
讓位,當前線程暫停,回到就緒狀態,讓給其它線程。
靜態方法:Thread.yield();
 */
public class ThreadTest12 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable6());
        t.setName("t");
        t.start();

        for(int i = 1; i <= 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

class MyRunnable6 implements Runnable {

    @Override
    public void run() {
        for(int i = 1; i <= 10000; i++) {
            //每100個讓位一次。
            if(i % 100 == 0){
                Thread.yield(); // 當前線程暫停一下,讓給主線程。
            }
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

Timer 定時器

方法名 說明
void schedule(TimerTask task, Date time) 在指定的時間安排指定的任務執行。
void schedule​(TimerTask task, Date firstTime, long period) 從指定的時間開始,對指定的任務執行重復的 固定延遲執行
/*
使用定時器指定定時任務。
 */
public class TimerTest {
    public static void main(String[] args) throws Exception {

        // 創建定時器對象
        Timer timer = new Timer();
        //Timer timer = new Timer(true); //守護線程的方式

        // 指定定時任務
        //timer.schedule(定時任務, 第一次執行時間, 間隔多久執行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2020-03-14 09:34:30");
        //timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
        // 每年執行一次。
        //timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);

        //匿名內部類方式
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                // code....
            }
        } , firstTime, 1000 * 10);

    }
}

// 編寫一個定時任務類
// 假設這是一個記錄日志的定時任務
class LogTimerTask extends TimerTask {

    @Override
    public void run() {
        // 編寫你需要執行的任務就行了。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":成功完成了一次數據備份!");
    }
}

線程同步

賣票【應用】

  • 案例需求

    某電影院目前正在上映國產大片,共有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();
      }
  }

死鎖【理解】

  • 概述

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

  • 什么情況下會產生死鎖

    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() 喚醒正在等待對象監視器的所有線程
  • wait方法作用:o.wait()讓正在o對象上活動的線程t進入等待狀態,並且釋放掉t線程之前占有的o對象的鎖。
  • notify方法作用:o.notify()讓正在o對象上等待的線程喚醒,只是通知,不會釋放o對象上之前占有的鎖。

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

  • 案例需求

    • 桌子類(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();
  
      }
  }

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

  • 阻塞隊列繼承結構

image

  • 常見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語句是我們自己寫的,打印語句沒有實現線程同步,所以可能打印的時候出現問題,這只是個小問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM