一、程序、進程、線程
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關鍵字能保證可見性、有序性(禁止指令重排),不能保證原子性。
5、CAS
(1)CAS是compare and swap的縮寫,即我們所說的比較交換。可以保證數據的原子性(是硬件對於並發操作的一個支持)。
(2)CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存地址里面的值和A的值是一樣的,那么就將內存里面的值更新成B,即 V == A ? V = B : V = V。
(3)可能會出現ABA問題,如線程1從內存X中取出A,這時候另一個線程2也從內存X中取出A,並且線程2進行了一些操作將內存X中的值變成了B,然后線程2又將內存X中的數據變成A,這時候線程1進行CAS操作發現內存X中仍然是A,然后線程1操作成功。雖然線程1的CAS操作成功,但是整個過程就是有問題的。比如鏈表的頭在變化了兩次后恢復了原值,但是不代表鏈表就沒有變化。為了解決ABA問題,可以在對象中額外再增加一個標記來標識對象是否有過變更,比如AtomicStampedReference/AtomicMarkableReference類。
八、同步實例
【模擬生產者消費者問題:】 問題: 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 b); } public class LambdaDemo { public static void main(String[] args) { Demo1 demo1 = () -> { System.out.println("Demo1"); }; demo1.show(); Demo1 demo12 = () -> System.out.println("Demo2"); demo12.show(); Demo2 demo3 = (int a, int b) -> { return a + b; }; System.out.println(demo3.show(3, 4)); Demo2 demo4 = (a, b) -> a + b; System.out.println(demo4.show(3, 4)); } }
十、TimerTask與Timer
1、TimerTask抽象類
java.util.TimerTask,TimerTask是一個抽象類,實現了Runnable接口。內部定義了一個抽象的run方法(public void run();)。其中run方法由子類實現,run方法中為需要被周期性運行的任務(即周期性的代碼)。
2、Timer類
java.util.Timer,有一個schedule方法,用於實現需要被周期性運行的程序。(定時任務,比如設置鬧鍾)
格式: schedule(TimerTask task, long delay, long period); 其中 task為具體實現TimerTask抽象類的run方法的子類。 delay表示schedule方法運行后到開始調度任務的延遲時間(單位為毫秒)。 period表示調度任務的周期,即每隔period時間執行一次代碼塊(單位為毫秒)。
import java.util.Timer; import java.util.TimerTask; class TimerTaskDemo extends TimerTask { @Override public void run() { System.out.println("TimerTaskDemo....."); System.out.println("當前系統時間為:" + System.currentTimeMillis()); System.out.println(); } } public class TimerDemo { public static void main(String[] args) { System.out.println("當前系統時間為:" + System.currentTimeMillis()); // timer對象啟動后,首先經過1000毫秒,第一次執行run()。然后每個2000毫秒執行一次run()。 Timer timer = new Timer(); timer.schedule(new TimerTaskDemo(), 1000, 2000); try { Thread.sleep(4000);//暫停4000毫秒 } catch (InterruptedException e) { e.printStackTrace(); } timer.cancel();// 取消Timer定時器任務 System.out.println("Timer Game Over....."); System.out.println("當前系統時間為:" + System.currentTimeMillis()); } } 【結果為:】 當前系統時間為:1561554413988 TimerTaskDemo..... 當前系統時間為:1561554414989 TimerTaskDemo..... 當前系統時間為:1561554416989 Timer Game Over..... 當前系統時間為:1561554417990
十一、高級多線程控制類
1、ThreadLocal類
(1)用於保存線程的獨立變量,使用ThreadLocal維護變量時,為每個使用該變量的線程提供獨立的變量副本,即每個線程可以隨意更改自己的變量副本,不會影響其他線程的變量副本。
(2)Thread內部有一個ThreadLocalMap類型的變量,屬於輕量級的Map,使用基本與map類似,但其桶內存放的是entry,而不是entry鏈表。
(3)主要方法:
public void set(T value); //在map集合里維護一個 Thread.currentThread()->value 鍵值對。
public T get(); //從map集合中取出 Thread.currentThread() 的value。
(4)實現類:
ThreadLocal,會開辟一個新的變量副本。
InheritableThreadLocal,其extends ThreadLocal,會拷貝上一個線程的變量。
public class ThreadLocalDemo { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); private static ThreadLocal<Double> threadLocal2 = new InheritableThreadLocal<Double>(); public static void main(String[] args) { /** * 測試ThreadLocal的get,set方法 */ // 設置main線程的名字為MainDemo Thread.currentThread().setName("MainDemo"); // 參數為Integer型,所以初始值為null,所以輸出MainDemo-->null System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get()); // 設置main線程保存的值為2 threadLocal.set(2); // 設置值為2,所以輸出MainDemo-->2 System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get()); /** * 此行代碼用於測試構造方法與run方法是由哪個線程調用 */ new Thread(new ThreadDemo(), "ThreadDemo").start(); /** * 測試ThreadLocal,InheritableThreadLocal的區別 */ // 設置main線程保存的值為100 threadLocal.set(100); // 設置值為100,所以輸出MainDemo-->100 System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get()); // 啟動一個線程ThreadLocalDemo new Thread(() -> { // ThreadLocal會重新創造一個變量副本,所以輸出ThreadLocalDemo-->null System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get()); threadLocal.set(50); new Thread(() -> { // ThreadLocal會重新創造一個變量副本,所以輸出ThreadLocalDemo1-->null System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get()); }, "ThreadLocalDemo1").start(); }, "ThreadLocalDemo").start(); // 設置main線程值為100.0 threadLocal2.set(100.0); // 設置值為100.0,所以輸出MainDemo-->100.0 System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get()); // 啟動一個線程InheritableThreadLocalDemo new Thread(() -> { // 其會拷貝上一個線程的值,上一個線程為main線程,所以輸出為InheritableThreadLocalDemo-->100.0 System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get()); threadLocal2.set(50.0); new Thread(() -> { // 其會拷貝上一個線程的值,上一個線程為InheritableThreadLocalDemo線程,所以輸出為InheritableThreadLocalDemo-->50.0 System.out.println(Thread.currentThread().getName() + "-->" + threadLocal2.get()); }, "InheritableThreadLocalDemo1").start(); }, "InheritableThreadLocalDemo").start(); } public static class ThreadDemo implements Runnable { public ThreadDemo() { // 初始化時,由main線程調用,所以輸出MainDemo-->2 System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get()); } @Override public void run() { // 運行時,由當前線程調用,會重新創建一個變量副本,所以輸出ThreadDemo-->null System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get()); } } }
Java1.5提供了一個非常高效實用的多線程包(java.util.concurrent)。
2、ThreadPoolExecutor 類
(1)java.util.concurrent.ThreadPoolExecutor 類就是一個線程池實現類。其最上層的接口為Executor,其有個核心方法 void execute(Runnable command)。Executor接口有一個子接口ExecutorService,ExecutorService的實現類為AbstractExecutorService,而ThreadPoolExcutor是AbstractExecutorService的子類。
public interface Executor { /** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thread, or in the calling * thread, at the discretion of the {@code Executor} implementation. * * @param command the runnable task * @throws RejectedExecutionException if this task cannot be * accepted for execution * @throws NullPointerException if command is null */ void execute(Runnable command); }
(2)ThreadPoolExecutor 構造方法
【構造方法:】 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } 【參數含義:】 corePoolSize:核心線程池的大小,如果核心線程池有空閑位置,此時新的任務就會使用核心線程池並新建一個線程執行,執行完畢后不會銷毀線程,線程會進入緩存隊列等待再次被運行。 maximunPoolSize:線程池能創建最大的線程數量。如果核心線程池和緩存隊列都已經滿了,新的任務進來就會創建新的線程來執行。但是數量不能超過maximunPoolSize,否側會采取拒絕接受任務策略。 keepAliveTime:非核心線程能夠空閑的最長時間,超過時間,線程終止。這個參數默認只有在線程數量超過核心線程池大小時才會起作用。只要線程數量不超過核心線程大小,就不會起作用。 unit:時間單位,和keepAliveTime配合使用。 workQueue:緩存隊列,用來存放等待被執行的任務。 threadFactory:線程工廠,用來創建線程。 handler:拒絕處理策略,線程數量大於最大線程數就會采用拒絕處理策略。
(3)線程池大小:
線程池內部維護的工作者線程的數量就是該線程池的線程池大小,有 3 種形態:
當前線程池大小 :表示線程池中實際工作者線程的數量。
最大線程池大小 (maxinumPoolSize):表示線程池中允許存在的工作者線程的數量上限;
核心線程大小 (corePoolSize ):表示最小工作的線程數。
如果運行的線程少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。
如果運行的線程等於或者多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不是添加新線程。
如果無法將請求加入隊列,即隊列已經滿了,則創建新的線程,除非創建此線程超出 maxinumPoolSize, 在這種情況下,任務將被拒絕。
(4)為什么使用線程池?
提供一個線程隊列,隊列中保存所有等待狀態的線程,減少創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
可以根據系統的承受能力,調整線程池中工作線程的數目,防止消耗過多的內存而導致死機。
(5)工具類(Executors)
public class Executors { // 創建一個固定大小的線程池 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } // 創建只有一個線程的線程池 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } // 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } // 創建一個定長線程池,支持定時及周期性任務執行。 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } }