1. 程式人生 > 實用技巧 >判斷一個日期所在的會計期間是否開啟

判斷一個日期所在的會計期間是否開啟

併發與並行

  • 併發:指兩個或多個事件在同一個時間段內發生。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。

在作業系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時執行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替執行,只不過是給人的感覺是同時執行,那是因為分時交替執行的時間是非常短的。

而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多工並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核 越多,並行處理的程式越多,能大大的提高電腦執行的效率。

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

執行緒與程序

  • 程序:是指一個記憶體中執行的應用程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時執行多個程序;程序也是程式的一次執行過程,是系統執行程式的基本單位;系統執行一個程式即是一個程序從建立、執行到消亡的過程。

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

    簡而言之:一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒

我們可以再電腦底部工作列,右鍵----->開啟工作管理員,可以檢視當前任務的程序:

程序

執行緒

執行緒排程:

  • 分時排程

    所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。

  • 搶佔式排程

    優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式排程。

    • 設定執行緒的優先順序

搶佔式排程詳解

大部分作業系統都支援多程序併發執行,現在的作業系統幾乎都支援同時執行多個程式。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板,dos視窗等軟體。此時,這些程式是在同時執行,”感覺這些軟體好像在同一時刻執行著“。
實際上,CPU(中央處理器)使用搶佔式排程模式在多個執行緒間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個執行緒,而 CPU的在多個執行緒間切換速度相對我們的感覺要快,看上去就是在同一時刻執行。
其實,多執行緒程式並不能提高程式的執行速度,但能夠提高程式執行效率,讓CPU的使用率更高。

多執行緒原理

建立執行緒方式一繼承Thread類

Java使用java.lang.Thread類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的例項。每個執行緒的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的程式碼。Java使用執行緒執行體來代表這段程式流。Java中通過繼承Thread類來建立啟動多執行緒的步驟如下:

  1. 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了執行緒需要完成的任務,因此把run()方法稱為執行緒執行體。
  2. 建立Thread子類的例項,即建立了執行緒物件
  3. 呼叫執行緒物件的start()方法來啟動該執行緒

程式碼如下:

測試類:

public class Demo01 {
	public static void main(String[] args) {
		//建立自定義執行緒物件
		MyThread mt = new MyThread("新的執行緒!");
		//開啟新執行緒
		mt.start();
		//在主方法中執行for迴圈
		for (int i = 0; i < 10; i++) {
			System.out.println("main執行緒!"+i);
		}
	}
}

自定義執行緒類:

public class MyThread extends Thread {
	//定義指定執行緒名稱的構造方法
	public MyThread(String name) {
		//呼叫父類的String引數的構造方法,指定執行緒的名稱
		super(name);
	}
	/**
	 * 重寫run方法,完成該執行緒執行的邏輯
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在執行!"+i);
		}
	}
}

建立執行緒方式二實現Runnable介面

採用 java.lang.Runnable 也是非常常見的一種,我們只需要重寫run方法即可。

步驟如下:

  1. 定義Runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
  2. 建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
  3. 呼叫執行緒物件的start()方法來啟動執行緒。
public class ThreadDemo02 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("我是子執行緒"+i);
        }
    }

    public static void main(String[] args) {
        ThreadDemo02 td2 = new ThreadDemo02();
        new Thread(td2).start();
        for (int i = 0; i <1000 ; i++) {
            System.out.println("我是主執行緒"+i);
        }
    }
}

通過實現Runnable介面,使得該類有了多執行緒類的特徵。run()方法是多執行緒程式的一個執行目標。所有的多執行緒 程式碼都在run方法裡面。
Thread類實際上也是實現了Runnable介面的類。

在啟動的多執行緒的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出物件,然後呼叫Thread 物件的start()方法
來執行多執行緒程式碼。實際上所有的多執行緒程式碼都是通過執行Thread的start()方法來執行的。因此,不管是繼承Thread類還是實現 Runnable
介面來實現多執行緒,最終還是通過Thread的物件的API來控制執行緒的,熟悉Thread類的API是進行多執行緒 程式設計的基礎。

tips:Runnable物件僅僅作為Thread物件的target,Runnable實現類裡包含的run()方法僅作為執行緒執行體。
而實際的執行緒物件依然是Thread例項,只是該Thread執行緒負責執行其target的run()方法。

Thread和Runnable的區別:

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。

總結:實現Runnable介面比繼承Thread類所具有的優勢:

  1. 適合多個相同的程式程式碼的執行緒去共享同一個資源。
  2. 可以避免java中的單繼承的侷限性。
  3. 增加程式的健壯性,實現解耦操作,程式碼可以被多個執行緒共享,程式碼和執行緒獨立。
  4. 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類。

擴充:在java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用 java命令執行一個
類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個程序。

匿名內部類方式實現執行緒的建立

使用執行緒的內匿名內部類方式,可以方便的實現每個執行緒執行不同的執行緒任務操作。

使用匿名內部類的方式實現Runnable介面,重新Runnable介面中的run方法:

/*
    匿名內部類方式實現執行緒的建立
    匿名:沒有名字
    內部類:寫在其他類內部的類
    匿名內部類作用:簡化程式碼
    把子類繼承父類,重寫父類的方法,建立子類物件合一步完成
    把實現類實現類介面,重寫介面中的方法,建立實現類物件合成一步完成
    匿名內部類的最終產物:子類/實現類物件,而這個類沒有名字
    格式:
    new 父類/介面(){
    重寫父類/介面中的方法
    };
 */
public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName()+"張三");
                }
            }
        }.start();

        /*Runnable r = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"李四");
            }
        };

        new Thread(r).start();*/

        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName()+"李四");
                }
            }
        }).start();
    }

執行緒安全

如果有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。程式每次執行結果和單執行緒執行的結果是一樣的,
而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

我們通過一個案例,演示執行緒的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個
(本場電影只能賣100張票)。 我們來模擬電影院的售票視窗,實現多個視窗同時賣 “葫蘆娃大戰奧特曼”這場電影票
(多個視窗一起賣這100張票) 需要視窗,採用執行緒物件來模擬;需要票,Runnable介面子類來模擬

public class RunnableImpl implements Runnable {
    private int ticketNums = 100;
    @Override
    public void run() {
        while (true){
            if (ticketNums>0){
                System.out.println(Thread.currentThread().getName()+"正在賣第"+ticketNums+"張票");
                ticketNums--;
            }
        }
    }
    public static void main(String[] args) {
        RunnableImpl r = new RunnableImpl();
        Thread t0 = new Thread(r,"視窗1");
        Thread t1 = new Thread(r,"視窗2");
        Thread t2 = new Thread(r,"視窗3");
        t0.start();
        t1.start();
        t2.start();
    }
}

執行緒同步

當我們使用多個執行緒訪問同一資源的時候,且多個執行緒中對資源有寫的操作,就容易出現執行緒安全問題。

要解決上述多執行緒併發訪問一個資源的安全性問題:也就是解決重複票與不存在票問題,Java中提供了同步機制 (synchronized)來解決。

根據案例簡述:

視窗1執行緒進入操作的時候,視窗2和視窗3執行緒只能在外等著,視窗1操作結束,視窗1和視窗2和視窗3才有機會進入程式碼去執行。
也就是說在某個執行緒修改共享資源的時候,其他執行緒不能修改該資源,等待修改完畢同步之後,才能去搶奪CPU 資源,完成對應的操作,
保證了資料的同步性,解決了執行緒不安全的現象。

為了保證每個執行緒都能正常執行原子操作,Java引入了執行緒同步機制。

那麼怎麼去使用呢?有三種方式完成同步操作:

  1. 同步程式碼塊。
  2. 同步方法。
  3. 鎖機制。

同步程式碼塊

  • 同步程式碼塊: synchronized關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。

格式:

synchronized(同步鎖){
需要同步操作的程式碼
}

同步鎖:

物件的同步鎖只是一個概念,可以想象為在物件上標記了一個鎖.

  1. 鎖物件可以是任意型別。
  2. 多個執行緒物件要使用同一把鎖。

注意:在任何時候,最多允許一個執行緒擁有同步鎖誰拿到鎖就進入程式碼塊,其他的執行緒只能在外等著
(BLOCKED)。

使用同步程式碼塊實現程式碼:

public class RunnableImpl implements Runnable {
    //定義一個多個執行緒共享的票源
    private int ticketNums = 100;
    //建立一個鎖物件
    Object obj = new Object();
    @Override
    public void run() {
        while (true){
            //建立同步程式碼塊
            synchronized (obj){
                if (ticketNums>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在賣第"+ticketNums+"張票");
                    ticketNums--;
                }
            }
        }
    }
    public static void main(String[] args) {
        RunnableImpl r = new RunnableImpl();
        Thread t0 = new Thread(r,"視窗1");
        Thread t1 = new Thread(r,"視窗2");
        Thread t2 = new Thread(r,"視窗3");
        t0.start();
        t1.start();
        t2.start();
    }
}

同步方法

  • 同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A執行緒執行該方法的時候其他執行緒只能在方法外等著。

格式:

public synchronized void method(){
可能會產生執行緒安全問題的程式碼
}

同步鎖是誰?
對於非static方法,同步鎖就是this.
對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)。
使用同步方法程式碼如下:

public class RunnableImpl03 implements Runnable {
    //定義一個多個執行緒共享的票源
    private int ticketNums = 100;
    @Override
    public void run() {
        while (true){
            payTicket();
        }
    }
    //建立同步方法
    /*
        鎖物件 是 誰呼叫這個方法 就是誰
        隱含 鎖物件 就是 this
     */
    public synchronized void payTicket(){
        if (ticketNums>0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在賣第"+ticketNums+"張票");
            ticketNums--;
        }
    }
    public static void main(String[] args) {
        RunnableImpl03 r = new RunnableImpl03();
        Thread t0 = new Thread(r,"視窗1");
        Thread t1 = new Thread(r,"視窗2");
        Thread t2 = new Thread(r,"視窗3");
        t0.start();
        t1.start();
        t2.start();
    }
}

Lock鎖

java.util. concurrent . locks. Lock機制提供了比synchronized程式碼塊和synchronized方法更廣泛的鎖定操作,
同步程式碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向物件。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

  • public void lock() :加同步鎖。
  • public void unlock() :釋放同步鎖。

使用如下:

public class RunnableImpl04 implements Runnable {
    //定義一個多個執行緒共享的票源
    private int ticketNums = 100;
    //建立ReentrantLock物件
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            //開啟鎖
            l.lock();
            if (ticketNums>0){
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName()+"正在賣第"+ticketNums+"張票");
                    ticketNums--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //釋放鎖
                    l.unlock();
                }
            }
        }
    }
    public static void main(String[] args) {
        RunnableImpl04 r = new RunnableImpl04();
        Thread t0 = new Thread(r,"視窗1");
        Thread t1 = new Thread(r,"視窗2");
        Thread t2 = new Thread(r,"視窗3");
        t0.start();
        t1.start();
        t2.start();
    }
}

執行緒狀態概述

執行緒狀態 導致狀態發生條件
NEW(新建) 執行緒剛被建立,但是並未啟動。還沒呼叫start方法。
Runnable(可執行) 執行緒可以在java虛擬機器中執行的狀態,可能正在執行自己程式碼,也可能沒有,這取決於作業系統處理器。
Blocked(鎖阻塞) 當一個執行緒試圖獲取一個物件鎖,而該物件鎖被其他的執行緒持有,則該執行緒進入Blocked狀態;當該執行緒持有鎖時,該執行緒將變成Runnable狀態。
Waiting(無限等待) 一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒。
TimedWaiting(計時等待) 同waiting狀態,有幾個方法有超時引數,呼叫他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時引數的常用方法有Thread.sleep 、Object.wait。
Teminated(被終止) 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

Timed Waiting (計時等待)

Timed Waiting在API中的描述為:一個正在限時等待另一個執行緒執行一個(喚醒)動作的執行緒處於這一狀態。單獨
的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪裡呢?

在我們寫賣票的案例中,為了減少執行緒執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就
強制當前正在執行的執行緒休眠(暫停執行),以"減慢執行緒”。

其實當我們呼叫了sleep方法之後,當前執行的執行緒就進入到”休眠狀態",其實就是所謂的Timed Waiting(計時等
待),那麼我們通過一個案例加深對該狀態的一 個理解。

實現一個計數器,計數到100, 在每個數字之間暫停1秒,每隔10個數字輸出一個字串

public class MyThread extends Thread {
      public void run( ) {
            for(inti=0;i<100;i++){
                  if((i)%10==0){
                        System. out . print1n("------”+ i);
                  }
            System . out . print(i);
            try {
                  Thread. sleep(1000);
                  System. out. print ("執行緒睡眠1秒! \n");
            } catch (InterruptedException e) {
                  e. printStackTrace();
            }
      }
}
      public static void main(String[] args) {
            new MyThread(). start();
      }
}

通過案例可以發現,sleep方法的使用還是很簡單的。我們需要記住下面幾點:

  1. 進入TIMED. _WAITING狀態的一種常見情形是呼叫的sleep方法,單獨的執行緒也可以呼叫,不一定非要有協
    作關係。
  2. 為了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣才能保證該執行緒執行過程
    中會睡眠
  3. sleep與鎖無關,執行緒睡眠到期自動甦醒,並返回到Runnable (可執行)狀態。

小提示: sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就
開始立刻執行。

Timed Waiting執行緒狀態圖:

BLOCKED (鎖阻塞)

Blocked狀態在API中的介紹為:一個正在阻塞等待一個監視器鎖(鎖物件)的執行緒處於這一狀態。

我們已經學完同步機制,那麼這個狀態是非常好理解的了。比如,執行緒A與執行緒B程式碼中使用同一鎖,如果執行緒A獲
取到鎖,執行緒A進入到Runnable狀態,那麼執行緒B就進入到Blocked鎖阻塞狀態。

這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態,而
這部分內容作為擴充知識點帶領大家瞭解一下。

Blocked執行緒狀態圖

Waiting (無限等待)

Wating狀態在API中介紹為:一個正在無限期等待另一個執行緒執行一個特別的(喚醒)動作的執行緒處於這一狀態.

public class Demo11WaitAndNotify {
    public static void main(String[] args) {
        //建立鎖物件,保證唯一
        Object obj = new Object();
        //建立顧客執行緒
        new Thread(){
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        System.out.println("告知老闆要的包子的種類和數量");
                        try {
                            //呼叫wait方法,放棄cpu的執行,進入到WAITING狀態(無限等待)
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("包子做好了,開吃");
                    }
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj){
                        System.out.println("花了5秒做包子");
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}

Waiting執行緒狀態圖

補充知識

執行緒間通訊

概念:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。

比如:執行緒A用來生成包子的,執行緒B用來吃包子的,包子可以理解為同一資源,執行緒A與執行緒B處理的動作,一個是生產,一個是消費,那麼執行緒A與執行緒B之間就存線上程通訊問題。

為什麼要處理執行緒間通訊:

多個執行緒併發執行時, 在預設情況下CPU是隨機切換執行緒的,當我們需要多個執行緒來共同完成一件任務,並且我們希望他們有規律的執行, 那麼多執行緒之間需要一些協調通訊,以此來幫我們達到多執行緒共同操作一份資料。

如何保證執行緒間通訊有效利用資源:

多個執行緒在處理同一個資源,並且任務不同時,需要執行緒通訊來幫助解決執行緒之間對同一個變數的使用或操作。 就是多個執行緒在操作同一份資料時, 避免對同一共享變數的爭奪。也就是我們需要通過一定的手段使各個執行緒能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制

什麼是等待喚醒機制

這是多個執行緒間的一種協作機制。談到執行緒我們經常想到的是執行緒間的競爭(race),比如去爭奪鎖,但這並不是故事的全部,執行緒間也會有協作機制。就好比在公司裡你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

就是在一個執行緒進行了規定操作後,就進入等待狀態(wait()), 等待其他執行緒執行完他們的指定程式碼過後 再將其喚醒(notify());在有多個執行緒進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待執行緒。

wait/notify 就是執行緒間的一種協作機制。

等待喚醒中的方法

等待喚醒機制就是用於解決執行緒間通訊的問題的,使用到的3個方法的含義如下:

  1. wait:執行緒不再活動,不再參與排程,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的執行緒狀態即是 WAITING。它還要等著別的執行緒執行一個特別的動作,也即是“通知(notify)”在這個物件上等待的執行緒從wait set 中釋放出來,重新進入到排程佇列(ready queue)中
  2. notify:則選取所通知物件的 wait set 中的一個執行緒釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
  3. notifyAll:則釋放所通知物件的 wait set 上的全部執行緒。

注意:

哪怕只通知了一個等待的執行緒,被通知執行緒也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它執行緒的競爭),成功後才能在當初呼叫 wait 方法之後的地方恢復執行。

總結如下:

  • 如果能獲取鎖,執行緒就從 WAITING 狀態變成 RUNNABLE 狀態;
  • 否則,從 wait set 出來,又進入 entry set,執行緒就從 WAITING 狀態又變成 BLOCKED 狀態

呼叫wait和notify方法需要注意的細節

  1. wait方法與notify方法必須要由同一個鎖物件呼叫。因為:對應的鎖物件可以通過notify喚醒使用同一個鎖物件呼叫的wait方法後的執行緒。

  2. wait方法與notify方法是屬於Object類的方法的。因為:鎖物件可以是任意物件,而任意物件的所屬類都是繼承了Object類的。

  3. wait方法與notify方法必須要在同步程式碼塊或者是同步函式中使用。因為:必須要通過鎖物件呼叫這2個方法。

生產者與消費者問題

等待喚醒機制其實就是經典的“生產者與消費者”的問題。

就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:

包子鋪執行緒生產包子,吃貨執行緒消費包子。當包子沒有時(包子狀態為false),吃貨執行緒等待,包子鋪執行緒生產包子(即包子狀態為true),
並通知吃貨執行緒(解除吃貨的等待狀態),因為已經有包子了,那麼包子鋪執行緒進入等待狀態。接下來,吃貨執行緒能否進一步執行則取決於鎖的
獲取情況。如果吃貨獲取到鎖,那麼就執行吃包子動作,包子吃完(包子狀態為false),並通知包子鋪執行緒(解除包子鋪的等待狀態),吃貨線
程進入等待。包子鋪執行緒能否進一步執行則取決於鎖的獲取情況。

程式碼演示:

包子資源類:

public class BaoZi {
     String  pier ;
     String  xianer ;
     boolean  flag = false ;//包子資源 是否存在  包子資源狀態
}

吃貨執行緒類:

public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//沒包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃貨正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}

包子鋪執行緒類:

public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子資源  存在
                    try {

                        bz.wait();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 沒有包子  造包子
                System.out.println("包子鋪開始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大蔥
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大蔥";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃貨來吃吧");
                //喚醒等待執行緒 (吃貨)
                bz.notify();
            }
        }
    }
}

測試類:

public class Demo {
    public static void main(String[] args) {
        //等待喚醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃貨",bz);
        BaoZiPu bzp = new BaoZiPu("包子鋪",bz);

        ch.start();
        bzp.start();
    }
}

執行效果:

包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃吧
吃貨正在吃薄皮牛肉大蔥包子
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子

執行緒池

執行緒池概念

  • 執行緒池:其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。

由於執行緒池中有很多操作都是與優化資源相關的,我們在這裡就不多贅述。我們通過一張圖來了解執行緒池的工作原理:

合理利用執行緒池能夠帶來三個好處:

  1. 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,
    而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,最後宕機)。

執行緒池的使用

Java裡面執行緒池的頂級介面是java.util.concurrent.Executor,但是嚴格意義上講Executor並不是一個執行緒池,而只是一個執行執行緒的工具。真正的執行緒池介面是java.util.concurrent.ExecutorService

要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,很有可能配置的執行緒池不是較優的,因此在java.util.concurrent.Executors執行緒工廠類裡面提供了一些靜態工廠,生成一些常用的執行緒池。官方建議使用Executors工程類來建立執行緒池物件。

Executors類中有個建立執行緒池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回執行緒池物件。(建立的是有界執行緒池,也就是池中的執行緒個數可以指定最大數量)

獲取到了一個執行緒池ExecutorService 物件,那麼怎麼使用呢,在這裡定義了一個使用執行緒池物件的方法如下:

  • public Future<?> submit(Runnable task):獲取執行緒池中的某一個執行緒物件,並執行

    Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用。

使用執行緒池中執行緒物件的步驟:

  1. 建立執行緒池物件。
  2. 建立Runnable介面子類物件。(task)
  3. 提交Runnable介面子類物件。(take task)
  4. 關閉執行緒池(一般不做)。

Runnable實現類程式碼:

public class RunnableImpl implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"建立了一個新的執行緒執行");
    }
}

執行緒池測試類:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //1.使用執行緒池的工廠類Executors裡邊提供的靜態方法newF ixedThreadPool生產一個指定執行緒數量的執行緒池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2.建立一個類, 實現Runnable介面,重寫run方法,設定執行緒任務
        //3.呼叫ExecutorService中的方法submit,傳遞執行緒任務(實現類),開啟執行緒,執行run方法
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
    }
}

函數語言程式設計思想概述

在數學中,函式就是有輸入量、輸出量的一套計算方案,也就是“拿什麼東西做什麼事情”。相對而言,面向物件過分強調“必須
通過物件的形式來做事情”,而函式式思想則儘量忽略面向物件的複雜語法——強調做什麼,而不是以什麼形式做

面向物件的思想:

​ 做一件事情,找一個能解決這個事情的物件,呼叫物件的方法,完成事情.

函數語言程式設計思想:

​ 只要能獲取到結果,誰去做的,怎麼做的都不重要,重視的是結果,不重視過程

冗餘的Runnable程式碼

傳統寫法

當需要啟動一個執行緒去完成任務時,通常會通過java.lang.Runnable介面來定義任務內容,並使用java.lang.Thread類來啟動該執行緒。程式碼如下:

public class Demo01Runnable {
	public static void main(String[] args) {
    	// 匿名內部類
		Runnable task = new Runnable() {
			@Override
			public void run() { // 覆蓋重寫抽象方法
				System.out.println("多執行緒任務執行!");
			}
		};
		new Thread(task).start(); // 啟動執行緒
	}
}

本著“一切皆物件”的思想,這種做法是無可厚非的:首先建立一個Runnable介面的匿名內部類物件來指定任務內容,再將其交給一個執行緒來啟動。

程式碼分析

對於Runnable的匿名內部類用法,可以分析出幾點內容:

  • Thread類需要Runnable介面作為引數,其中的抽象run方法是用來指定執行緒任務內容的核心;
  • 為了指定run的方法體,不得不需要Runnable介面的實現類;
  • 為了省去定義一個RunnableImpl實現類的麻煩,不得不使用匿名內部類;
  • 必須覆蓋重寫抽象run方法,所以方法名稱、方法引數、方法返回值不得不再寫一遍,且不能寫錯;
  • 而實際上,似乎只有方法體才是關鍵所在

程式設計思想轉換

做什麼,而不是怎麼做

我們真的希望建立一個匿名內部類物件嗎?不。我們只是為了做這件事情而不得不建立一個物件。我們真正
希望做的事情是:將run方法體內的程式碼傳遞給Thread類知曉。

傳遞一段程式碼——這才是我們真正的目的。而建立物件只是受限於面向物件語法而不得不採取的一種手段方式。
那,有沒有更加簡單的辦法?如果我們將關注點從“怎麼做”迴歸到“做什麼”的本質上,就會發現只要能夠更好地達
到目的,過程與形式其實並不重要。

生活舉例

當我們需要從北京到上海時,可以選擇高鐵、汽車、騎行或是徒步。我們的真正目的是到達上海,而如何才能到達
上海的形式並不重要,所以我們一直在探索有沒有比高鐵更好的方式——搭乘飛機。

而現在這種飛機(甚至是飛船)已經誕生:2014年3月Oracle所釋出的Java 8(JDK 1.8)中,加入了Lambda表示式
的重量級新特性,為我們打開了新世界的大門。

體驗Lambda的更優寫法

藉助Java 8的全新語法,上述Runnable介面的匿名內部類寫法可以通過更簡單的Lambda表示式達到等效:

public class Demo02LambdaRunnable {
	public static void main(String[] args) {
		new Thread(() -> System.out.println("多執行緒任務執行!")).start(); // 啟動執行緒
	}
}

這段程式碼和剛才的執行效果是完全一樣的,可以在1.8或更高的編譯級別下通過。從程式碼的語義中可以看出:我們啟動了一個執行緒,
而執行緒任務的內容以一種更加簡潔的形式被指定。

不再有“不得不建立介面物件”的束縛,不再有“抽象方法覆蓋重寫”的負擔,就是這麼簡單!

回顧匿名內部類

Lambda是怎樣擊敗面向物件的?在上例中,核心程式碼其實只是如下所示的內容:

() -> System.out.println("多執行緒任務執行!")

為了理解Lambda的語義,我們需要從傳統的程式碼起步。

使用實現類

要啟動一個執行緒,需要建立一個Thread類的物件並呼叫start方法。而為了指定執行緒執行的內容,需要呼叫Thread類的構造方法:

  • public Thread(Runnable target)

為了獲取Runnable介面的實現物件,可以為該介面定義一個實現類RunnableImpl

public class RunnableImpl implements Runnable {
	@Override
	public void run() {
		System.out.println("多執行緒任務執行!");
	}
}

然後建立該實現類的物件作為Thread類的構造引數:

public class Demo03ThreadInitParam {
	public static void main(String[] args) {
		Runnable task = new RunnableImpl();
		new Thread(task).start();
	}
}

使用匿名內部類

這個RunnableImpl類只是為了實現Runnable介面而存在的,而且僅被使用了唯一一次,所以使用匿名內部類的語法即可省去該類的單獨定義,即匿名內部類:

public class Demo04ThreadNameless {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("多執行緒任務執行!");
			}
		}).start();
	}
}

匿名內部類的好處與弊端

一方面,匿名內部類可以幫我們省去實現類的定義;另一方面,匿名內部類的語法——確實太複雜了!

語義分析

仔細分析該程式碼中的語義,Runnable介面只有一個run方法的定義:

  • public abstract void run();

即制定了一種做事情的方案(其實就是一個函式):

  • 無引數:不需要任何條件即可執行該方案。
  • 無返回值:該方案不產生任何結果。
  • 程式碼塊(方法體):該方案的具體執行步驟。

同樣的語義體現在Lambda語法中,要更加簡單:

() -> System.out.println("多執行緒任務執行!")
  • 前面的一對小括號即run方法的引數(無),代表不需要任何條件;
  • 中間的一個箭頭代表將前面的引數傳遞給後面的程式碼;
  • 後面的輸出語句即業務邏輯程式碼。

Lambda標準格式

Lambda省去面向物件的條條框框,格式由3個部分組成:

  • 一些引數
  • 一個箭頭
  • 一段程式碼

Lambda表示式的標準格式為:

(引數型別 引數名稱) -> { 程式碼語句 }

格式說明:

  • 小括號內的語法與傳統方法引數列表一致:無引數則留空;多個引數則用逗號分隔。
  • ->是新引入的語法格式,代表指向動作。
  • 大括號內的語法與傳統方法體要求基本一致。

練習:使用Lambda標準格式(無參無返回)

題目

給定一個廚子Cook介面,內含唯一的抽象方法makeFood,且無引數、無返回值。如下:

public interface Cook {
    void makeFood();
}

在下面的程式碼中,請使用Lambda的標準格式呼叫invokeCook方法,列印輸出“吃飯啦!”字樣:

public class Demo05InvokeCook {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【標準格式】呼叫invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

解答

public static void main(String[] args) {
    invokeCook(() -> {
      	System.out.println("吃飯啦!");
    });
}

備註:小括號代表Cook介面makeFood抽象方法的引數為空,大括號代表makeFood的方法體。

Lambda的引數和返回值

需求:
    使用陣列儲存多個Person物件
    對陣列中的Person物件使用Arrays的sort方法通過年齡進行升序排序

下面舉例演示java.util.Comparator<T>介面的使用場景程式碼,其中的抽象方法定義為:

  • public abstract int compare(T o1, T o2);

當需要對一個物件陣列進行排序時,Arrays.sort方法需要一個Comparator介面例項來指定排序的規則。假設有一個Person類,含有String nameint age兩個成員變數:

public class Person { 
    private String name;
    private int age;
    
    // 省略構造器、toString方法與Getter Setter 
}
public class LambdaDemo02 {
    public static void main(String[] args) {
        Person[] arr = {
                new Person("馬雲",56),
                new Person("馬化騰",48),
                new Person("雷軍",45),
        };
        /*Arrays.sort(arr, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge()-o2.getAge();
            }
        });*/
        //使用lambda方式
        Arrays.sort(arr,(Person o1, Person o2)->{
            return o1.getAge()-o2.getAge();
        });
        for (Person p : arr) {
            System.out.println(p);
        }
    }
}

執行結果:

Person{name='雷軍', age=45}
Person{name='馬化騰', age=48}
Person{name='馬雲', age=56}

練習:使用Lambda標準格式(有參有返回)

題目

給定一個計算器Calculator介面,內含抽象方法calc可以將兩個int數字相加得到和值:

public interface Calculator {
    int calc(int a, int b);
}

在下面的程式碼中,請使用Lambda的標準格式呼叫invokeCalc方法,完成120和130的相加計算:

public class Demo08InvokeCalc {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【標準格式】呼叫invokeCalc方法來計算120+130的結果ß
    }

    private static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println("結果是:" + result);
    }
}

解答

public static void main(String[] args) {
    invokeCalc(120, 130, (int a, int b) -> {
      	return a + b;
    });
}

備註:小括號代表Calculator介面calc抽象方法的引數,大括號代表calc的方法體。

練習:使用Lambda省略格式

Lambda表示式:是可推導,可以省略
凡是根據上下文推匯出來的內容,都可以省略書寫

可以省略的內容:

  1. (引數列表) :括號中引數列表的資料型別,可以省略不寫
  2. (引數列表) :括號中的引數如果只有一個那麼型別和( )都可以省略
  3. {一些程式碼} :如果{}中的程式碼只有一行,無論是否有返回值都可以省略({},return,分號)

注意:要省略{}, return,分號必須一起省略

題目

仍然使用前文含有唯一makeFood抽象方法的廚子Cook介面,在下面的程式碼中,請使用Lambda的省略格式呼叫invokeCook方法,列印輸出“吃飯啦!”字樣:

public class Demo09InvokeCook {
    public static void main(String[] args) {
        // TODO 請在此使用Lambda【省略格式】呼叫invokeCook方法
    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

解答

public static void main(String[] args) {
  	invokeCook(() -> System.out.println("吃飯啦!"));
}

Lambda的使用前提

Lambda的語法非常簡潔,完全沒有面向物件複雜的束縛。但是使用時有幾個問題需要特別注意:

  1. 使用Lambda必須具有介面,且要求介面中有且僅有一個抽象方法
    無論是JDK內建的RunnableComparator介面還是自定義的介面,只有當介面中的抽象方法存在且唯一時,才可以使用Lambda。
  2. 使用Lambda必須具有上下文推斷
    也就是方法的引數或區域性變數型別必須為Lambda對應的介面型別,才能使用Lambda作為該介面的例項。

備註:有且僅有一個抽象方法的介面,稱為“函式式介面”。