Java基礎--多線程

来源:https://www.cnblogs.com/l-y-h/archive/2019/06/26/11087534.html
-Advertisement-
Play Games

一、程式、進程、線程 1、區別 (1)程式是一段靜態的代碼,為應用程式執行的藍本。 (2)進程為程式的一次動態執行過程,包括代碼的載入、執行以及執行完畢的一個完整過程。 (3)線程是進程中的一個執行單元,一個進程在執行過程中可以產生多個線程(至少有一個線程 )。 2、關係 (1)進程負責的是應用程式 ...


一、程式、進程、線程

1、區別

  (1)程式是一段靜態的代碼,為應用程式執行的藍本。
  (2)進程為程式的一次動態執行過程,包括代碼的載入、執行以及執行完畢的一個完整過程。
  (3)線程是進程中的一個執行單元,一個進程在執行過程中可以產生多個線程(至少有一個線程 )。

2、關係

  (1)進程負責的是應用程式的空間的標識,線程負責的是應用程式的執行順序。
  (2)進程擁有一個包含了某些資源的記憶體區域,多個線程間共用進程的記憶體。
  (3)線程的中斷與恢復相比於進程可以節省系統的開銷。
  (4)進程是資源分配的基本單位,線程是調度和執行的基本單位。

3、為什麼使用線程

  (1)使用多線程可以減少程式的響應時間。(把耗時的線程讓一個單獨線程去解決)
  (2)線程創建與切換的開銷比進程小。
  (3)在能運行多線程的機器上運行單線程,會造成資源的浪費。
  (4)多線程能簡化程式結構,使其便於理解。

 

二、多線程

1、多線程指的是一個進程中同時存在幾個執行體(線程),按照不同的執行順序共同工作的現象。

 

2、多線程並不是同時發生,系統在任何時刻只能執行一個線程,只是java虛擬機快速的將控制從一個線程切換到另一個線程(多個線程輪流執行),造成同時發生的錯覺。

 

3、每個java程式都有一個預設的主進程,當JVM啟動時,發現main方法後,會啟動一個主線程(main線程),用於執行main方法。若main方法中沒有創建其他線程,那麼當main執行完最後一個語句時,JVM會結束java應用程式。若main方法中創建了其他進程,那麼JVM會等到所有線程結束後再結束java應用程式。

 

4、同步與非同步:

  (1)同步就是指一個線程要等待上一個線程執行完之後才開始執行當前的線程。非同步指的是一個線程的執行不需要在意其他線程的執行。

  (2)同步要解決的問題是當多個線程訪問同一個資源時,多個線程間需要以某種順序來確保該資源在某時刻只被一個線程使用,解決辦法是獲取線程對象的鎖,得到鎖,則這個線程進入臨界區(訪問互斥資源的代碼塊),且鎖未釋放前,其他線程不能進入此臨界區。但同步機制會帶來巨大的系統開銷,甚至死鎖,所以要儘量避免無謂的同步。

  (3)簡單的講,同步就是A喊B去吃飯,如果B聽到,就和A一起去吃飯,若B沒聽到,則A就一直喊,直到B聽到,然後在一起去吃飯。 而非同步是A喊B去吃飯,然後A自己去吃飯,不管B接下來的動作,B可能與A一起吃飯,也可能過了幾個小時再去吃飯。

  (4)說的直白點,同步就是執行有先後順序,A執行完B再執行,非同步就是各乾各的,A執行一部分,B執行一部分,A與B間沒有聯繫。

 

5、並行與併發:

  (1)並行:多個cpu實例或者多台機器同時執行一段處理邏輯,是真正的同時執行。
  (2)併發:通過cpu調度演算法,讓各線程快速切換。使程式看上去是同時執行的,

 

6、線程安全與不安全:

  (1)線程安全:指在併發條件下,代碼經過多線程的調用,各線程的調度順序 不影響代碼執行結果。
  (2)線程不安全:即指線程的調度順序影響代碼執行結果。

 

7、Java的記憶體模型(JMM):

  (1)併發程式中,確保數據訪問的一致性以及安全性非常重要。在瞭解並行機制的前提下,並定義一種規則,保證多個線程間可以有效地、正確地協同工作,從而產生了JMM。
  (2)Java記憶體模型規範了Java虛擬機與電腦記憶體是如何協同工作的。JMM的核心均圍繞多線程的原子性、可見性、有序性來建立的。
  (3)Java記憶體模型規定瞭如何和何時可以看到由其他線程修改過後的共用變數的值,以及在必須時如何同步的訪問共用變數。

 

8、原子性、可見性、有序性:

  (1)原子性:指的是一個操作是不可中斷的。可以理解為一個操作要麼執行成功,要麼不執行。
  (2)可見性:指的是一個線程修改了某一個共用變數的值,則其他線程能立即知道這個修改。
  (3)有序性:併發執行時,程式經過 指令重排後(提高cpu處理性能),程式的執行可能會亂序。通過指定 指令重排規則,使程式的邏輯有序,即不改變代碼邏輯。

註:指令重排:執行代碼的順序與編寫代碼的順序不一致,即虛擬機優化代碼,改變代碼順序。

 

9、線程的run()與start()方法的區別:

  (1)系統通過調用start()方法來啟動一個線程,此時該線程處於就緒狀態,需要等JVM調度,然後調用run()方法,線程才進入運行狀態。即調用start()方法是一個非同步調用的過程。

  (2)若直接調用run()方法,則相當於調用一個普通方法,不能實現多線程。即調用run()方法是一個同步的過程。

舉例:
System.out.println("1");
Thread thread = new Thread(new Runnable() {

    @Override
    public void run() {
       System.out.println("2");
   }
 });
thread.start(); //啟動線程,等待JVM調度,不一定會立即執行。
System.out.println("3");
此時,非同步,輸出1, 3, 2

System.out.println("1");
Thread thread = new Thread(new Runnable() {

    @Override
    public void run() {
       System.out.println("2");
   }
 });
thread.run(); //不啟動線程,立即執行。
System.out.println("3");
此時,同步,按順序執行,輸出1, 2, 3

 

三、線程的生命周期

1、java使用Thread類及其子類的對象來表示線程。

 

2、線程的生命周期通常為 新建狀態,就緒狀態,運行狀態,阻塞狀態,死亡狀態。

 

3、新建狀態:

  當Thread類及其子類的對象被聲明並創建後,新線程處於新建狀態,此時該線程有了相應的記憶體空間和其他資源。

 

4、就緒狀態:

  調用Thread類的start()方法,用於啟動線程,使線程進入線程隊列排隊等待JVM調度。

 

5、運行狀態:

  線程被創建後,就具備了運行條件。但該線程僅僅占有記憶體空間,JVM管理的線程中還沒有這個線程。需要調用start()方法(從父類繼承的方法),通知JVM有新的線程等待切換。切換成功後,該線程會脫離創建他的主線程,並開始新的生命周期。如果此線程是Thread的子類創建的,由於Thread類中的run()方法沒有具體內容,所以其子類必須重寫run()方法(run()方法包含線程運行的代碼)。

 

6、阻塞狀態(三種):

  如果一個線程執行了sleep(睡眠)、suspend(掛起,不推薦使用)等方法,失去所占用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。
  (1)等待阻塞狀態:
    運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。等待阻塞狀態不會主動進入線程隊列排隊等待,需要由其他線程調用notify()方法通知它,才能進入線程隊列排隊等待。

 

  (2)同步阻塞狀態:
    線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。

 

  (3)其他阻塞狀態:
    通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。

 

7、死亡狀態(兩種):

  死亡狀態指線程釋放了實體,即釋放分配給線程對象的記憶體。
  (1)正常運行的線程完成run()方法。
  (2)線程被強制結束run()方法。

 

四、多線程的創建

1、 方法一:繼承Thread(不常用),由子類覆寫run方法。

  使用子類創建優缺點:可以在子類中拓展新的成員變數與新方法,使線程具備某種特性與功能。但不能再擴展其他類,因為java不支持多繼承。即單繼承局限性。
步驟:
1、定義類去繼承Thread類;
2、覆寫run方法,將線程運行的代碼寫在run方法中。
3、通過創建Thread類的子類對象(實現多態),創建線程對象。
4、調用線程的start方法,開啟線程,並執行run方法。

【定義】
class MyThread extends Thread{
    @Override
    public void run() {
    }
}
【執行】
Thread myThread1 = new MyThread();
myThread1.start();

【舉例】
class Demo extends Thread {

    @Override
    public void run() {
        System.out.println("1");
    }
}

public class Test {
    public static void main(String[] args) {
        Thread demo = new Demo();
        demo.start();
    }
}

 

2、方法二:實現一個介面Runnable(常用)

  實現Runnable介面避免單繼承的局限性。
步驟:
1、定義類實現Runnable介面。
2、覆蓋介面中的run方法,封裝線程要運行的代碼。
3、通過Thread類創建對象。
4、將實現了Runnable介面的子類對象作為實際參數傳遞給Thread類中的構造函數。
5、調用Thread對象的start方法,開啟線程,執行介面中的run方法。

【定義】
class MyRunnable implements Runnable{
    @Override
    public void run() {
    }
}
【執行】
Runnable myRunnable = new MyRunnable();
Thread myThread1 = new Thread(myRunnable);
myThread1.start();

【舉例】
public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {

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

 

3、方法三:通過Callable和Future創建線程

  前兩種的缺點:在執行完任務之後無法獲取執行結果。如果需要獲取執行結果,就必須通過共用變數或者使用線程通信的方式來達到效果,這樣使用起來就比較麻煩。
自從Java 1.5開始,就提供了Callable和Future介面,通過它們可以在任務執行完畢之後得到任務執行結果。
  創建Callable介面的實現類,並實現call()方法。並使用FutureTask類(實現了Runnable,Future介面)來包裝Callable實現類的對象,且以此FutureTask對象作為Thread對象的target來創建線程。通過FutureTask類的get()方法可以獲取處理結果。

【定義】
class MyCallable implements Callable<Integer>{    
    @Override
    public Integer call() throws Exception {
        return null;
    }
}
【執行】
Callable<Integer> myCallable = new MyCallable();
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
Thread myThread1 = new Thread(ft);
myThread1.start();

【舉例】
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Demo implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        Integer integer = 1;
        return integer;
    }

}

public class Test {
    public static void main(String[] args) {
        Callable<Integer> callable = new Demo();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

 

五、多線程常用方法:

1、線程名:

  一個線程的預設名字為“Thread-“ + 數字,可以通過函數來修改。
  (1)public final synchronized void setName(String name); // 設定線程名
  (2)public final String getName(); //獲取線程名

2、線程ID:

  public long getId(); // 獲取線程的ID

 

3、線程優先順序:

  處於就緒狀態的線程會首先進入就緒隊列等待CPU資源,而同一時刻存在就緒隊列中的線程可能有多個。java虛擬機(JVM)中的線程調度器負責管理線程,調度器將線程的優先順序分為1~10,並使用Thread類中的類常量表示,即Thread.MIN_PRIORITY~Thread.MAX_PRIORITY。若未明確設置線程優先順序,則預設為5,即Thread.NORM_PRIORITY。線程優先順序不能保證線程執行的順序,其依賴於平臺。
  (1)public final void setPriority(int newPriority); //設置線程優先順序,若newPriority不在1~10之間,會拋出java.lang.IllegalArgumentException異常。
  (2)public final int getPriority(); //獲取線程的優先順序。

4、常用方法:

  (1)public synchronized void start(); // 此方法用於啟動線程,使新建狀態的線程進入就緒隊列排序。只有新建狀態的線程才可調用start()方法,且只能調用一次。再次調用會拋出java.lang.IllegalArgumentException異常。

  (2)public void run(); //需要重寫,用於定義線程對象被調用後的操作,是系統自動調用的方法。

  (3)public static native void sleep(long millis) throws InterruptedException; //線程的調度是根據優先順序執行的,且總是執行高優先順序。若想在高優先順序未死亡時使用低優先順序,可以調用sleep()方法將高優先順序線程中斷,參數millis的單位為毫秒。若休眠被打斷,會拋出java.lang.IllegalArgumentException異常。所以必須在try-catch語句中使用sleep()方法。

  (4) public final native boolean isAlive(); //線程處於新建狀態時,線程調用isAlive方法返回false。當線程調用start()方法並占有CPU資源後,此時run開始執行,在run未結束前,isAlive方法返回true。線程死亡後,isAlive方法返回false。如果一個正在運行的線程還未死亡,不能再為該線程分配新實體,因為線程只能引用最後分配的實體,其最初的實體不會被垃圾回收器回收。因為垃圾回收期認定先前的實體為運行狀態,若回收會引起錯誤,所以不回收。

  (5)public static native Thread currentThread(); //屬於Thread中的類方法,可直接使用類名調用,用於返回當前正在使用CPU資源的線程。

  (6)public final void join() throws InterruptedException; //一個線程A的運行期間,可以使用join()方法來聯合線程B,即在A線程中啟動並運行(join)B線程,當B線程結束後,A線程再繼續運行。

  (7)public static native void yield(); //從線程從運行狀態立即進入就緒狀態

5、中斷方法

  (1)public void interrupt(); //中斷線程,將會設置該線程的中斷狀態位,即設置為true,中斷的結果線程是死亡、還是等待新的任務或是繼續運行至下一步,就取決於這個程式本身(即不會中斷線程)。線程會不時地檢測這個中斷標示位,以判斷線程是否應該被中斷(中斷標示值是否為true)。它並不像stop方法那樣會中斷一個正在運行的線程。

  (2) public static boolean interrupted(); //測試當前線程是否被中斷(檢查中斷標誌),返回一個boolean並清除中斷狀態,第二次再調用時中斷狀態已經被清除,將返回一個false。

  (3)public boolean isInterrupted(); //測試線程是否被中斷 ,不清除中斷狀態。

若線程在阻塞狀態(通過wait, join, sleep方法)時,調用了它的interrupt()方法,
那麼它的“中斷狀態”會被清除並且會收到一個InterruptedException異常。
例如,線程通過wait()進入阻塞狀態,此時通過interrupt()中斷該線程;
調用interrupt()會立即將線程的中斷標記設為“true”,但是由於線程處於阻塞狀態,
所以該“中斷標記”會立即被清除為“false”,
同時,會產生一個InterruptedException的異常。

【通用處理】
@Override
public void run() {
    try {
        /*
        isInterrupted()用於判斷線程的中斷標記是不是為true。
        當線程處於運行狀態,並且我們需要終止它時;可以調用線程的interrupt()方法,
        使線程的中斷標記為true,即isInterrupted()會返回true。
        此時,就會退出while迴圈。
        */
        // 1. isInterrupted()保證,只要中斷標記為true就終止線程。
        while (!isInterrupted()) {
            // 執行任務...
        }
    } catch (InterruptedException ie) { 
        // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。
    }
}

 

六、線程同步

  同步就是指一個線程要等待上一個線程執行完之後才開始執行當前的線程。
  線程同步指的是通過人為的調控,保證多線程訪問共用資源的方式是線程安全的(可以通過synchronized關鍵字實現)。
  線程同步實質上是一種等待機制,多個線程需要同時訪問某對象時,會進入該對象的等待隊列中排隊,等待前麵線程調用結束後,再開始訪問該對象。核心理念就是等待隊列 + 鎖機制。

1、synchronized關鍵字

  (1)Java中每個對象有且僅有一個同步鎖。
  (2)當調用某對象的synchronized方法時,就獲取了該對象的同步鎖。
  (3)不同線程對同步鎖的訪問是互斥的。即某個時間點,對象的同步鎖只能被一個線程獲得。

2、synchronized基本規則

  (1)當一個線程訪問某對象的“synchronized方法或代碼塊”時,其他線程對該對象的“synchronized方法或代碼塊”的訪問將被阻塞。
  (2)當一個線程訪問某對象的“synchronized方法或代碼塊”時,其他線程仍然可以訪問該對象的非同步代碼塊。
  (3)當一個線程訪問某對象的“synchronized方法或代碼塊”時,其他線程對該對象的其他的“synchronized方法或代碼塊”的訪問將被阻塞。

3、synchronized的用法:

  (1)指定加鎖對象:即給指定對象加鎖,進入同步代碼前要先獲取給定對象的鎖。

class Demo{
    Object obj = new Object();
    synchronized(obj){ //此時對obj對象有個同步鎖。
    }
}

  (2)直接作用於實例方法:相當於給當前實例加鎖,進入同步代碼前要先獲得當前實例的鎖。

即
Demo demo = new Demo();
demo.show();//此時對Demo類的demo實例有個同步鎖。

class Demo{
    public synchronized void show(){
    }
}

  (3)直接作用於靜態方法:相當於給當前類加鎖,進入同步代碼前要先獲得當前類的鎖。

即
Demo.show();  //此時對Demo類有個同步鎖,即對所有demo實例均有鎖。
class Demo{
    public static synchronized void show(){
    }
}

 

4、Object方法:

  (1)public final void wait() throws InterruptedException; //讓當前線程進入等待狀態,直到其他線程調用此對象的notify() 或 notifyAll() 方法,當前線程被喚醒(進入“就緒狀態”) 。同時,wait()也會讓當前線程釋放它所持有的鎖。

  (2)public final native void wait(long timeout) throws InterruptedException; //在wait()方法基礎上,當等待超過指定時間量也會進入就緒狀態。(如果時間為0,等價於wait(),則無限等待!)

  (3)public final void wait(long timeout, int nanos) throws InterruptedException; //在wait(long)方法基礎上,提供納秒級別的時間精度。

  (4)public final native void notify(); //隨機喚醒在此對象監視器上等待的單個線程。

  (5)public final native void notifyAll(); //喚醒在此對象監視器上等待的所有線程。

 

5、實現線程同步:

  (1)通過synchronized關鍵字,可以修飾方法以及代碼塊。
  (2)通過wait()方法以及notify(),notifyAll()方法。wait需寫在try-catch中。
  (3)jdk1.5後推出,通過Lock介面以及其實現類ReentrantLock(重入鎖)。

 

七、常見問題

1、sleep()與wait()的區別:

  (1)sleep是Thread類的靜態方法,自動喚醒。而wait是Object類的方法,需通過notify方法喚醒。
  (2)sleep是讓線程暫停一段時間,時間一到,自動喚醒,不涉及線程間通信,故不釋放所占用的鎖。而wait方法會釋放自己的鎖,且其synchronized數據能被其他線程調用,涉及線程間通信。
  (3)由於sleep不會釋放鎖,易導致死鎖問題,所以一般推薦使用wait方法。

死鎖:指的是兩個或兩個線程在執行過程中,因爭奪資源而造成的一種相互等待的現象,若無外力作用,則無法向前推進。

 

2、sleep()與yield()的區別:

  (1)調用sleep方法後,其他線程的優先順序不考慮,此時低優先順序線程有運行機會。調用yield方法後,只有相同或更高的優先順序線程才能有機會運行。
  (2)執行sleep方法後,線程會進入阻塞狀態,必須等待一段時間後才會進入執行狀態。執行yield方法後,線程會回到可執行狀態,所以可能立刻執行。
  (3)sleep方法可能拋出InterruptedException異常,而yield沒有異常。

 

3、守護線程:

  (1)java提供兩種線程,一個是守護線程(daemon),一個是用戶線程。
  (2)守護線程又稱為服務線程,精靈線程,後臺線程,指在程式運行時在後臺提供一種通用服務的線程。守護線程不是程式不可或缺的部分。
  (3)若用戶線程全部結束,即程式中只剩守護線程,那麼JVM也就退出了,並殺死所有守護線程,即程式運行結束。
  (4)可以自己設置守護進程,需在start()方法前調用setDaemon(true)方法,若參數為false,則表示用戶進程。
  (5)GC就是運行在一個守護線程上。

 

4、volatile關鍵字:

  (1)為了提高程式處理效率,數據不會直接與硬體進行交互,設置一個緩存區,將主存的數據拷貝到緩存中,在此處理數據,處理完成後,再寫入主存中。
  (2)針對多線程使用的變數,如果未使用final或者volatile關鍵字修飾,很可能產生不可預知的結果。比如一個靜態變數int i = 0,線程A修改 i = 1,在緩存中修改,但未立即寫入主存中(理解為修改未生效), 此時線程B訪問的仍是 i = 0,則導致出現錯誤。
  (3)使用volatile關鍵字修飾的變數,每次修改後,直接寫入主存,其緩存中的內容無效,每次從主存中讀取數據。
  (4)volatile關鍵字能保證可見性、有序性(禁止指令重排),不能保證原子性。

 

八、同步實例

【模擬生產者消費者問題:】
問題:
1、有一個果籃,生產者可以向果籃中放置蘋果,消費者每次從果籃中拿一個蘋果,並花一段時間吃蘋果。
2、當果籃中蘋果數為0時,消費者停止拿蘋果,並通知生產者放入一定數量的蘋果。
3、當果籃中蘋果數大於0時,通知消費者拿蘋果。
4、生產者每次通過控制台輸入蘋果數。

【果籃類,FruitBasket.java】
/**
 * 果籃類,用於存放蘋果。
 *
 */
public class FruitBasket {
    private int appleNumber;// 果籃中蘋果數

    /**
     * 構造方法,根據指定蘋果數初始化果籃。
     * 
     * @param appleNumber
     *            蘋果數
     */
    public FruitBasket(int appleNumber) {
        this.appleNumber = appleNumber;
    }

    /**
     * 同步方法,由於修飾的是非靜態方法,所以鎖對象為果籃類的實例。 獲得果籃中的蘋果數
     * 
     * @return 蘋果數
     */
    public synchronized int getAppleNumber() {
        return appleNumber;
    }

    /**
     * 同步方法,由於修飾的是非靜態方法,所以鎖對象為果籃類的實例。 設置果籃中的蘋果數
     * 
     * @param appleNumber
     *            蘋果數
     */
    public synchronized void setAppleNumber(int appleNumber) {
        this.appleNumber = appleNumber;
    }

    /**
     * 同步方法,由於修飾的是非靜態方法,所以鎖對象為果籃類的實例。 消費者每次從果籃中拿一個蘋果,果籃中蘋果數每次減1。
     * 
     */
    public synchronized void decreaseNumber() {
        appleNumber--;
    }

    /**
     * 同步方法,由於修飾的是非靜態方法,所以鎖對象為果籃類的實例。生產者每次放置一定的蘋果數,果籃中蘋果數每次增加一定數量。
     * 
     * @param number
     *            蘋果數
     */
    public synchronized void increaseNumber(int number) {
        appleNumber += number;
    }
}


【生產者類,Producer.java】
import java.util.Scanner;

/**
 * 生產者,生產蘋果,並放置到果籃中。 通過控制台輸入放置的蘋果數。
 *
 */
public class Producer implements Runnable {

    private FruitBasket fruitBasket;// 定義一個果籃,用於存放蘋果數
    private Scanner scanner;// 用於獲取控制台輸入的數據

    /**
     * 構造方法,用於初始化一個生產者。
     * 
     * @param fruitBasket
     *            果籃類
     * @param scanner
     *            輸入類
     */
    public Producer(FruitBasket fruitBasket, Scanner scanner) {
        this.fruitBasket = fruitBasket;
        this.scanner = scanner;
    }

    /**
     * 重寫run方法,用於實現生產者放蘋果的邏輯。
     */
    @Override
    public void run() {
        while (true) {// 迴圈執行
            // 以果籃實例為同步鎖,每次只允許一個生產者或者消費者線程進行操作。
            synchronized (fruitBasket) {
                System.out.println("\n========================================================");
                System.out.println("當前果籃中蘋果數為: " + fruitBasket.getAppleNumber());
                try {
                    System.out.println("生產者向果籃中放置蘋果: ");
                    // 獲取輸入的蘋果數
                    int number = Integer.parseInt(scanner.next());
                    // 向果籃中添加蘋果
                    fruitBasket.increaseNumber(number);
                    System.out.println("放置蘋果完成,當前果籃中蘋果數為:  " + fruitBasket.getAppleNumber());

                    // 如果果籃中蘋果數大於0,則通知消費者來拿蘋果
                    if (fruitBasket.getAppleNumber() > 0) {
                        System.out.println("通知消費者來拿蘋果。");
                        fruitBasket.notifyAll();// 通知所有的消費者
                        fruitBasket.wait();// 生產者等待
                    }
                } catch (NumberFormatException e) {
                    System.out.println("輸入的數據格式錯誤,請重新輸入。");
                } catch (InterruptedException e) {
                    System.out.println("系統異常");
                }
                System.out.println();
            }
        }
    }

}


【消費者類,Consumer.java】
/**
 * 消費者,每次從果籃中拿一個蘋果。每拿一個蘋果,需要吃一段時間。
 *
 */
public class Consumer implements Runnable {

    private FruitBasket fruitBasket;// 定義一個果籃,用於存放蘋果數
    private long seconds;// 定義吃蘋果時間
    private String name;// 消費者名

    /**
     * 根據果籃,休眠時間,名字來初始化一個消費者。
     * 
     * @param fruitBasket
     *            果籃
     * @param seconds
     *            吃蘋果時間
     * @param name
     *            消費者名
     */
    public Consumer(FruitBasket fruitBasket, long seconds, String name) {
        this.fruitBasket = fruitBasket;
        this.seconds = seconds;
        this.name = name;
    }

    /**
     * 重寫方法,用於實現消費者拿蘋果的邏輯。
     */
    @Override
    public void run() {
        while (true) {// 迴圈執行
            // 以果籃實例為同步鎖,每次只允許一個生產者或者消費者線程進行操作。
            synchronized (fruitBasket) {
                // 如果果籃中蘋果數為0,
                if (fruitBasket.getAppleNumber() == 0) {
                    try {
                        // 通知生產者放置蘋果。
                        fruitBasket.notifyAll();
                        fruitBasket.wait();// 消費者進行等待
                    } catch (InterruptedException e) {
                        System.out.println("系統錯誤");
                    }
                } else {// 如果果籃中蘋果數大於0
                    System.out.println("\n========================================================");
                    System.out.println("當前果籃中蘋果數為:  " + fruitBasket.getAppleNumber());
                    System.out.println(name + " 開始拿蘋果");
                    fruitBasket.decreaseNumber();// 果籃中蘋果數減1
                    System.out.println(name + " 拿完蘋果,當前果籃中蘋果數為:  " + fruitBasket.getAppleNumber());
                    try {
                        System.out.println(name + " 開始吃蘋果……");
                        long start = System.currentTimeMillis();
                        Thread.sleep(seconds * 1000);// 吃蘋果的時間
                        long end = System.currentTimeMillis();
                        System.out.println(name + " 吃完蘋果了:  " + (end - start));
                        fruitBasket.notifyAll();// 喚醒生產者和消費者
                        fruitBasket.wait();// 當前消費者等待
                    } catch (InterruptedException e) {
                        System.out.println("系統錯誤");
                    }
                }
                System.out.println();
            }
        }
    }

}


【測試類,ProducerAndConsumerDemo.java】
import java.util.Scanner;

/**
 * 測試功能。 使用3個線程表示消費者,1個線程表示生產者。 在果籃中沒有蘋果時,消費者停止拿蘋果的操作,通知生產者放置蘋果(通過控制台輸入)。
 * 當果籃中有蘋果時,通知消費者開始拿蘋果,消費者每次拿完蘋果後會花一定的時間吃蘋果。
 *
 */
public class ProducerAndConsumerDemo {
    // 實例化一個果籃,用於存放蘋果
    private static FruitBasket fruitBasket = new FruitBasket(0);
    // 實例化一個輸入實例,用於獲取控制台輸入
    private static Scanner scanner = new Scanner(System.in);

    /**
     * 測試入口
     * 
     * @param args
     */
    public static void main(String[] args) {
        // 實例化一個生產者
        Thread producer = new Thread(new Producer(fruitBasket, scanner));
        producer.start();// 啟動生產者

        // 實例化一個消費者A
        Thread consumerA = new Thread(new Consumer(fruitBasket, 3, "consumerA"));
        consumerA.start();// 啟動消費者A

        // 實例化一個消費者B
        Thread consumerB = new Thread(new Consumer(fruitBasket, 2, "consumerB"));
        consumerB.start();// 啟動消費者B

        // 實例化一個消費者C
        Thread consumerC = new Thread(new Consumer(fruitBasket, 4, "consumerC"));
        consumerC.start();// 啟動消費者C
    }
}

 

【模擬電影院購票問題:】
import java.util.HashSet;
import java.util.Set;

/**
 * 電影院類
 */
class Cinema {

    private Set<Integer> seats;// 用於保存當前電影院還存在的位置
    private String cinemaName;// 用於保存電影院的名字

    /**
     * 構造方法,用於構造電影院
     * 
     * @param seats
     *            電影院當前還存在的位置
     * @param cinemaName
     *            電影院的名字
     */
    public Cinema(Set<Integer> seats, String cinemaName) {
        this.seats = seats;
        this.cinemaName = cinemaName;
    }

    /**
     * 獲取電影院存在的座位位置
     * 
     * @return 電影院的座位位置
     */
    public Set<Integer> getSeats() {
        return seats;
    }

    /**
     * 設置電影院的座位位置
     * 
     * @param seats
     *            電影院的座位位置
     */
    public void setSeats(Set<Integer> seats) {
        this.seats = seats;
    }

    /**
     * 獲取電影院的名字
     * 
     * @return 電影院的名字
     */
    public String getCinemaName() {
        return cinemaName;
    }

    /**
     * 設置電影院的名字
     * 
     * @param cinemaName
     *            電影院的名字
     */
    public void setCinemaName(String cinemaName) {
        this.cinemaName = cinemaName;
    }

    /**
     * 進行購票操作
     * 
     * @param buySeats
     *            需要購買的座位
     * @return true 表示購票成功 false 表示購票失敗
     */
    public boolean buyTicket(Set<Integer> buySeats) {
        // 複製一份電影院座位表
        Set<Integer> copy = new HashSet<Integer>();
        copy.addAll(seats);

        // 減去被購買的座位
        copy.removeAll(buySeats);

        // 存在座位
        if (seats.size() - copy.size() == buySeats.size()) {
            seats.removeAll(buySeats);
            return true; // 成功購票
        }
        return false;// 不存在座位,購票失敗
    }

    /**
     * 輸出電影院剩餘座位表
     */
    public void showTicket() {
        System.out.println("電影院當前剩餘座位表如下:");
        for (Integer integer : seats) {
            System.out.print(integer + " ");
        }
        System.out.println();
        System.out.println();
    }
}

/**
 * 顧客類
 */
class Customer implements Runnable {

    private Set<Integer> buySeatSet; // 用於保存需要購買的座位
    private Cinema cinema;// 用於保存電影院

    /**
     * 構造方法,初始化顧客
     * 
     * @param buySeatSet
     *            購買的座位數
     * @param cinema
     *            電影院
     */
    public Customer(Set<Integer> buySeatSet, Cinema cinema) {
        this.buySeatSet = buySeatSet;
        this.cinema = cinema;
    }

    /**
     * 獲取需要購買的座位
     * 
     * @return 需要購買的座位
     */
    public Set<Integer> getBuySeatSet() {
        return buySeatSet;
    }

    /**
     * 設置需要購買的座位
     * 
     * @param buySeatSet
     *            需要購買的座位
     */
    public void setBuySeatSet(Set<Integer> buySeatSet) {
        this.buySeatSet = buySeatSet;
    }

    /**
     * 獲取電影院
     * 
     * @return 電影院
     */
    public Cinema getCinema() {
        return cinema;
    }

    /**
     * 設置電影院
     * 
     * @param cinema
     *            電影院
     */
    public void setCinema(Cinema cinema) {
        this.cinema = cinema;
    }

    @Override
    public void run() {
        synchronized (cinema) { // 對電影院加個鎖,每次只允許一個顧客成功買票
            if (buySeatSet.size() > cinema.getSeats().size()) {
                System.out.println("電影院餘票不足, 當前餘票數為: " + cinema.getSeats().size());
            } else {
                if (cinema.buyTicket(buySeatSet)) {
                    System.out.println(Thread.currentThread().getName() + "購票成功!");
                } else {
                    System.out.println("當前位置不存在," + Thread.currentThread().getName() + "購票失敗!");
                }
            }
            cinema.showTicket();
        }
    }
}

/**
 * 測試類,用來模擬電影院購票操作
 */
public class TicketPurchaseDemo {

    public static void main(String[] args) {
        Set<Integer> seats = new HashSet<Integer>();
        for (int i = 1; i <= 10; i++) {
            seats.add(i);
        }
        Cinema cinema = new Cinema(seats, "Wanda");
        cinema.showTicket();

        Set<Integer> buySeats1 = new HashSet<Integer>();
        buySeats1.add(1);
        buySeats1.add(3);
        new Thread(new Customer(buySeats1, cinema), "孫悟空").start();

        Set<Integer> buySeats2 = new HashSet<Integer>();
        buySeats2.add(1);
        buySeats2.add(2);
        new Thread(new Customer(buySeats2, cinema), "豬八戒").start();

        Set<Integer> buySeats3 = new HashSet<Integer>();
        buySeats3.add(2);
        buySeats3.add(4);
        new Thread(new Customer(buySeats3, cinema), "唐三藏").start();
    }
}

 

九、Lambda表達式簡單使用

1、採用函數式介面(介面中只有一個方法),避免匿名內部類定義過多。步驟為先實現介面中的方法,然後再調用該方法。
2、規則:

【格式:】
    ()->{}。
若出現一個參數的情況,可以將()省略。
若出現一行代碼的情況,可以將{}省略。
對於多個參數的情況,可以省略參數類型,但()不能省略。
若{}中只有一行代碼,且為return語句,則可省略return。

【舉例:】
interface Demo1 {
    void show();
}

interface Demo2 {
    int show(int a, int	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. uni app插件ColorUI步驟條 1.1. 前言 1. uni app就不介紹了,前面幾篇已經有所介紹,不知道的可以翻看我前面幾篇博客 2. "ColorUI uniApp" 是uni app的一款ui組件,事實上就是對uni app組件添加css,使其更加漂亮 3. 這裡我拋磚引玉的介 ...
  • 用法 Vue.js 允許你自定義過濾器,可被用於一些常見的文本格式化。過濾器可以用在兩個地方:雙花括弧插值和 v-bind 表達式 (後者從 2.1.0+ 開始支持)。過濾器應該被添加在 JavaScript 表達式的尾部,例如: 渲染結果為: 當我們在頁面里輸出某些數據,需要進行格式轉換的時候可以 ...
  • 版本2.4.5 問題展示: 存在問題:正好錯位一個縱向滾動條的寬度 思路: 仔細觀察th元素及th包裹的子元素div 如下圖 發現th寬度莫名的就多了5px 我就納悶了 解決方案:到table.js源碼中→搜索 →縱向滾動條寬度,找到了,並按下圖修改 解決了 解決後效果如下 ...
  • [TOC] 環境介紹 軟體版本:ElasticSearch7.0.0 Kibana7.0.0 系統環境:mac 環境 安裝過程 "官網" 下載 ElasticSearch7.0.0 版本,下載後解壓即可 進入es安裝目錄 啟動 es 如果報錯的話直接給整個目錄增加讀寫許可權 在頁面中訪問 "http: ...
  • 今天的每日一碼的題目講的是判斷一個數是不是迴文數,所謂的迴文數就是不論是從左往右讀還是從右往左讀都是一樣的結果,比方說12321。方法有很多,這裡和大家一起分享幾個。 主要可以從兩個方面來解決吧:一個是把輸入看成是一串字元串,然後利用字元串的一些函數來進行處理。個人覺得這類方法可以記一記,在面試的時 ...
  • Array: Arrays.copyfOf(<original primitive array>, int newLengtg) 如果newlength大於original, 則會padd成0或者null character,取決於original array 的type. ...
  • 所屬網站分類: 資源下載 > python腳本 作者:皇後娘娘別惹我 鏈接: http://www.pythonheidong.com/blog/article/285/ 來源:python黑洞網 www.pythonheidong.com 作者:皇後娘娘別惹我 鏈接: http://www.pyt ...
  • 本文將使用 putty 連接到一臺阿裡雲 Ubuntu 16.04 伺服器,在其上安裝 go 語言的編譯環境,旨在呈現從安裝到“你好,世界!”涉及的方方面面,希望完成這個過程無須覓它處。 1. 安裝 方式一使用 apt get 執行完成之後,會把 golang 安裝在這個位置: ,go 命令會在該目 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...