判斷一個日期所在的會計期間是否開啟
併發與並行
- 併發:指兩個或多個事件在同一個時間段內發生。
- 並行:指兩個或多個事件在同一時刻發生(同時發生)。
在作業系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時執行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替執行,只不過是給人的感覺是同時執行,那是因為分時交替執行的時間是非常短的。
而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多工並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核 越多,並行處理的程式越多,能大大的提高電腦執行的效率。
注意:單核處理器的計算機肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發執行。同理,執行緒也是一樣的,從巨集觀角度上理解執行緒是並行執行的,但是從微觀角度上分析卻是序列執行的,即一個執行緒一個執行緒的去執行,當系統只有一個CPU時,執行緒會以某種順序執行多個執行緒,我們把這種情況稱之為執行緒排程。
執行緒與程序
-
程序:是指一個記憶體中執行的應用程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時執行多個程序;程序也是程式的一次執行過程,是系統執行程式的基本單位;系統執行一個程式即是一個程序從建立、執行到消亡的過程。
-
執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。一個程序中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。
簡而言之:一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒
我們可以再電腦底部工作列,右鍵----->開啟工作管理員,可以檢視當前任務的程序:
程序
執行緒
執行緒排程:
-
分時排程
所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。
-
搶佔式排程
優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式排程。
- 設定執行緒的優先順序
搶佔式排程詳解
大部分作業系統都支援多程序併發執行,現在的作業系統幾乎都支援同時執行多個程式。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板,dos視窗等軟體。此時,這些程式是在同時執行,”感覺這些軟體好像在同一時刻執行著“。
實際上,CPU(中央處理器)使用搶佔式排程模式在多個執行緒間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個執行緒,而 CPU的在多個執行緒間切換速度相對我們的感覺要快,看上去就是在同一時刻執行。
其實,多執行緒程式並不能提高程式的執行速度,但能夠提高程式執行效率,讓CPU的使用率更高。
多執行緒原理
建立執行緒方式一繼承Thread類
Java使用java.lang.Thread
類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的例項。每個執行緒的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的程式碼。Java使用執行緒執行體來代表這段程式流。Java中通過繼承Thread類來建立並啟動多執行緒的步驟如下:
- 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了執行緒需要完成的任務,因此把run()方法稱為執行緒執行體。
- 建立Thread子類的例項,即建立了執行緒物件
- 呼叫執行緒物件的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方法即可。
步驟如下:
- 定義Runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
- 建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
- 呼叫執行緒物件的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類所具有的優勢:
- 適合多個相同的程式程式碼的執行緒去共享同一個資源。
- 可以避免java中的單繼承的侷限性。
- 增加程式的健壯性,實現解耦操作,程式碼可以被多個執行緒共享,程式碼和執行緒獨立。
- 執行緒池只能放入實現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引入了執行緒同步機制。
那麼怎麼去使用呢?有三種方式完成同步操作:
- 同步程式碼塊。
- 同步方法。
- 鎖機制。
同步程式碼塊
- 同步程式碼塊: synchronized關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:
synchronized(同步鎖){
需要同步操作的程式碼
}
同步鎖:
物件的同步鎖只是一個概念,可以想象為在物件上標記了一個鎖.
- 鎖物件可以是任意型別。
- 多個執行緒物件要使用同一把鎖。
注意:在任何時候,最多允許一個執行緒擁有同步鎖誰拿到鎖就進入程式碼塊,其他的執行緒只能在外等著
(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方法的使用還是很簡單的。我們需要記住下面幾點:
- 進入TIMED. _WAITING狀態的一種常見情形是呼叫的sleep方法,單獨的執行緒也可以呼叫,不一定非要有協
作關係。 - 為了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣才能保證該執行緒執行過程
中會睡眠 - 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個方法的含義如下:
- wait:執行緒不再活動,不再參與排程,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的執行緒狀態即是 WAITING。它還要等著別的執行緒執行一個特別的動作,也即是“通知(notify)”在這個物件上等待的執行緒從wait set 中釋放出來,重新進入到排程佇列(ready queue)中
- notify:則選取所通知物件的 wait set 中的一個執行緒釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
- notifyAll:則釋放所通知物件的 wait set 上的全部執行緒。
注意:
哪怕只通知了一個等待的執行緒,被通知執行緒也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它執行緒的競爭),成功後才能在當初呼叫 wait 方法之後的地方恢復執行。
總結如下:
- 如果能獲取鎖,執行緒就從 WAITING 狀態變成 RUNNABLE 狀態;
- 否則,從 wait set 出來,又進入 entry set,執行緒就從 WAITING 狀態又變成 BLOCKED 狀態
呼叫wait和notify方法需要注意的細節
-
wait方法與notify方法必須要由同一個鎖物件呼叫。因為:對應的鎖物件可以通過notify喚醒使用同一個鎖物件呼叫的wait方法後的執行緒。
-
wait方法與notify方法是屬於Object類的方法的。因為:鎖物件可以是任意物件,而任意物件的所屬類都是繼承了Object類的。
-
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();
}
}
執行效果:
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃吧
吃貨正在吃薄皮牛肉大蔥包子
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
執行緒池
執行緒池概念
- 執行緒池:其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。
由於執行緒池中有很多操作都是與優化資源相關的,我們在這裡就不多贅述。我們通過一張圖來了解執行緒池的工作原理:
合理利用執行緒池能夠帶來三個好處:
- 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
- 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
- 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,
而把伺服器累趴下(每個執行緒需要大約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介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用。
使用執行緒池中執行緒物件的步驟:
- 建立執行緒池物件。
- 建立Runnable介面子類物件。(task)
- 提交Runnable介面子類物件。(take task)
- 關閉執行緒池(一般不做)。
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 name
和int 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表示式:是可推導,可以省略
凡是根據上下文推匯出來的內容,都可以省略書寫
可以省略的內容:
- (引數列表) :括號中引數列表的資料型別,可以省略不寫
- (引數列表) :括號中的引數如果只有一個那麼型別和( )都可以省略
- {一些程式碼} :如果{}中的程式碼只有一行,無論是否有返回值都可以省略({},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的語法非常簡潔,完全沒有面向物件複雜的束縛。但是使用時有幾個問題需要特別注意:
- 使用Lambda必須具有介面,且要求介面中有且僅有一個抽象方法。
無論是JDK內建的Runnable
、Comparator
介面還是自定義的介面,只有當介面中的抽象方法存在且唯一時,才可以使用Lambda。 - 使用Lambda必須具有上下文推斷。
也就是方法的引數或區域性變數型別必須為Lambda對應的介面型別,才能使用Lambda作為該介面的例項。
備註:有且僅有一個抽象方法的介面,稱為“函式式介面”。