JavaSE(十一)——多线程
文章目錄
- 1. 進程和線程
- 1.1 串行和并行
- 1.2 并發和并行
- 1.3 進程和線程
- 1.4 線程的五種狀態
- 1.5 線程池
- 2. 線程對象
- 2.1 線程創建方式一:繼承Thread類
- 2.2 線程創建方法二:實現Runnable接口
- 2.3 線程的命名
- 2.4 線程優先級
- 2.5 線程休眠:sleep方法
- 2.6 線程禮讓:yield方法
- 2.7 線程聯合:join方法
- 2.8 線程停止:stop方法
- 2.9 守護線程
- 3. 線程同步
- 3.1 線程沖突
- 3.2 同步方法與同步代碼塊
- 4. 線程死鎖
- 5. 線程協調
- 6. 高級并發對象
- 6.1 線程定義
- 6.2 線程同步:鎖對象
- 6.3 線程池
- 6.4 并發集合:BLockingQueue
- 6.5 靜態代理
- 6.6 Lambda表達式
1. 進程和線程
1.1 串行和并行
串行:指多個任務時,各個任務按順序執行,完成一個之后才能進行下一個。
并行:指多個任務可以同時執行。異步是多個任務并行的前提。
1.2 并發和并行
并發:在操作系統中,是指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。其中兩種并發關系分別是同步和互斥,
- 互斥:進程間相互排斥的使用臨界資源的現象,就叫互斥。
- 同步:進程之間的關系不是相互排斥臨界資源的關系,而是相互依賴的關系。進一步的說明:就是前一個進程的輸出作為后一個進程的輸入,當第一個進程沒有輸出時第二個進程必須等待。具有同步關系的一組并發進程相互發送的信息稱為消息或事件。
- 異步:異步和同步是相對的,同步就是順序執行,執行完一個再執行下一個,需要等待、協調運行。異步就是彼此獨立,在等待某事件的過程中繼續做自己的事,不需要等待這一事件完成后再工作。線程就是實現異步的一個方式。
并行:在操作系統中,一組程序按獨立異步的速度執行,無論從微觀還是宏觀,程序都是一起執行的。
來個比喻:并發和并行的區別就是一個人同時吃三個饅頭和三個人同時吃三個饅頭;
1.3 進程和線程
進程:具有獨立的執行環境和一套完整的私有基本運行時資源,每個進程都有自己的存儲空間。
線程:有時稱為輕量級進程,創建新線程所需的資源少于創建新進程的資源。
進程與線程的區別:
- 進程是資源分配最小單位,線程是程序執行的最小單位;
- 進程有自己獨立的地址空間,每啟動一個進程,系統都會為其分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,線程沒有獨立的地址空間,它使用相同的地址空間共享數據;
- CPU切換一個線程比切換進程花費小;
- 創建一個線程比進程開銷小;
- 線程占用的資源要?進程少很多;
- 線程之間通信更方便,同一個進程下,線程共享全局變量,靜態變量等數據,進程之間的通信需要以通信的方式(IPC)進行;(但多線程程序處理好同步與互斥是個難點);
- 多進程程序更安全,生命力更強,一個進程死掉不會對另一個進程造成影響(源于有獨立的地址空間),多線程程序更不易維護,一個線程死掉,整個進程就死掉了(因為共享地址空間);
- 進程對資源保護要求高,開銷大,效率相對較低,線程資源保護要求不高,但開銷小,效率高,可頻繁切換。
線程狀態如下所示:
1.4 線程的五種狀態
(1)新建( new ):新創建了一個線程對象。
(2)可運行( runnable ):線程對象創建后,其他線程(比如 main 線程)調用了該對象的 start ()方法。該狀態的線程位于可運行線程池中,等待被線程調度選中,獲取 cpu 的使用權 。
(3)運行( running ):可運行狀態( runnable )的線程獲得了 cpu 時間片( timeslice ) , 執行程序代碼。
(4)阻塞( block ):阻塞狀態是指線程因為某種原因放棄了 cpu 使用權,如當調用sleep()、wait()方法或同步鎖時,線程進入阻塞狀態,不再往下執行。阻塞事件解除后,線程重新進入可運行( runnable )狀態,才有機會再次獲得 cpu 使用權轉到運行( running )狀態。
(5)死亡( dead ):線程 run ()、 main () 方法執行結束,或者因異常退出了 run ()方法,則 該線程結束生命周期。死亡的線程不可再次復生。
1.5 線程池
線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建, 使用完畢不需要銷毀線程而是放回池中,從而減少創建和銷毀線程對象的開銷。
2. 線程對象
創建線程的三種方式:繼承Thread類,實現Runnable接口,實現Callable接口
2.1 線程創建方式一:繼承Thread類
步驟:
(1)自定義線程類繼承Thread類
(2)重寫run()方法,編寫線程執行體
(3)創建線程對象,調用start()方法啟動線程
2.2 線程創建方法二:實現Runnable接口
步驟:
(1)自定義一個類實現Runnable接口
(2)重寫run()方法,編寫線程執行體
(3)創建線程對象,調用start()方法啟動線程
注意:實現 Runnable 接口這種方式更受歡迎,因為這不需要繼承 Thread 類。在應用設計中已經繼承了 別的對象的情況下,這需要多繼承(而 Java 不支持多繼承),只能實現接口。
- 多線程有兩種實現方法,分別是繼承Thread類與實現Runnable接口;兩者在啟動線程時不同,繼承Thread類是子類對象.start(),而實現Runnable接口是傳入目標對象+Thread對象.start()
- 同步的實現方面有兩種, 分別是 synchronized, wait 與 notify。
2.3 線程的命名
(1)Thread thread = new Thread(helloRunnable,“我是子線程1”);
案例:
(2)Thread thread = new Thread(helloRunnable);
thread.setName(“我是子線程1”);
案例:
2.4 線程優先級
線程優先級的范圍是1~10,默認的優先級是5,最高級是10。“高優先級線程”被分配CPU的概率高于“低優先級線程”。
//線程的優先級用數字表示 Thread.MIN_PRIORITY=1; Thread.MAX_PRIORITY=10; Thread.NORM_PRIORITY=5;//獲取優先級 getPriority();//設置優先級大小 setPriority(int xxx);實例:
2.5 線程休眠:sleep方法
Thread.sleep() 使當前線程在指定時間段內暫停執行。
實例:
2.6 線程禮讓:yield方法
Thread.yield() 給當前正處于運行狀態下的線程一個提醒,告知它可以將資源禮讓給其他線程,但這僅僅是一種暗示,沒有任何一種機制保證當前線程會將資源禮讓。
實例:
sleep()和yield()方法的區別:
-
sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;
-
yield()方法只會給相同優先級或更高優先級的線程以運行的機會。
2.7 線程聯合:join方法
join方法允許一個線程等待另一個線程的完成。如果t是Thread正在執行其線程對象,t.join()導致當前線程暫停執行,直到 t 的線程終止。
類似于插隊,如下列vip線程插隊到主線程
package Thread;public class TestJoin implements Runnable {@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println("我是vip線程--->"+i);}}public static void main(String[] args) throws InterruptedException {TestJoin testJoin=new TestJoin();Thread thread = new Thread(testJoin);thread.start();//主線程for (int i = 0; i < 1000; i++) {if(i==100){thread.join(); //vip線程插隊到主線程}System.out.println("我是主線程--->"+i);}} }運行結果:
2.8 線程停止:stop方法
停止一個線程意味著在線程處理任務完成之前停掉正在做的操作,也就是放棄當前操作。
推薦使用退出標識,使得線程正常退出,即當run方法完成后進程終止。
實例:
2.9 守護線程
- Java線程分為用戶線程和守護線程兩種;
- 用戶線程是系統的工作線程,它會完成這個程序要完成的業務員操作;
- 守護線程是一種特殊的線程,是系統的守護者,在后臺默默完成一些系統性的服務,比如垃圾回收線程。
如果用戶線程全部結束,則意味著這個程序無事可做。守護線程要守護的對象已經不存在了,那么整個應用程序就結束了。
此處實例:龜兔賽跑
3. 線程同步
在多線程程序中,會出現多個線程搶占一個資源的情況,即出現并發現象,這時間有可能會造成沖突,也就是一個線程可能還沒來得及將更改的資源保存,另一個線程的更改就開始了。可能造成數據不一致。因此引入多線程同步,也就是說多個線程只能一個對共享的資源進行更改,其他線程不能對數據進行修改。
3.1 線程沖突
當在不同線程中運行作用于相同數據的兩個操作時,就會發生干擾,這意味著這兩個操作由多個步驟組成,并且步驟順序重疊。
- 檢索的當前值c
- 將檢索到的值加1
- 將增加的值存儲回c
此處實例:售票員們賣100張票
3.2 同步方法與同步代碼塊
synchronized用于解決同步問題,當有多條線程同時訪問共享數據時,如果進行同步,就會發生錯誤,Java提供的解決方案是:只要將操作共享數據的語句在某一時段讓一個線程執行完,在執行過程中,其他線程不能進來執行可以。解決這個問題。這里在用synchronized時會有兩種方式,一種是上面的同步方法,即用synchronized來修飾方法,另一種是提供的同步代碼塊。
//同步方法 public synchronized void method1(){}//同步代碼塊 public void method2(){synchronized (Obj){} //其中Obj稱為同步監視器,可以是任何對象 }常見面試題:synchronized 和 java.util.concurrent.locks.Lock 的異同
- 主要相同點:Lock 能完成 synchronized 所實現的所有功能。
- 主要不同點:
(1)synchronized 是托管給 JVM 執行的,lock 的鎖定是通過代碼實現的,它有比 synchronized 更精確的線程語義。
(2)synchronized 會自動釋放鎖,而 Lock 一定要求程序員手工釋放 ,并且必須在 finally 語句中釋放。
(3)synchronized 既可以加在方法上,也可以加載特定代碼塊上,而Lock 需要顯示地指定起始位置和終止位置。
4. 線程死鎖
-
死鎖是指多個線程各自占有一些共享資源,并且相互等待其他線程占有的資源才能運行,導致線程出現死鎖。
-
死鎖不僅僅在線程之間會發生,存在資源獨占的進程之間同樣也可能出現死鎖。通常來說,我們大多是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,由于互相持有對方需要的鎖,互相等待,而永久處于阻塞狀態。
-
使用多線程的時候,一種非常簡單的避免死鎖的方式就是:指定獲取鎖的順序,并強制線程按照指定的順序獲取鎖。因此,如果所有的線程都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖了。
5. 線程協調
-
wait()方法和notify()方法;
-
發生死鎖的條件
- 互斥條件
- 請求保持條件
- 不剝奪條件
- 循環等待條件
懶漢式單例模式:
- 在使用這個對象時,才去查看這個對象是否創建。如果沒創建就馬上創建;如果已經創建,就返回這個實例。
- 線程不安全,需要加上同步鎖,影響了程序執行效率。
餓漢式單例模式:
- 在加載這個類的時候,就先創建好一個對象實例,等待調用。
- 天生線程安全,類加載的時候初始化一次對象,效率比懶漢式高
6. 高級并發對象
6.1 線程定義
- 實現Callable接口
前面兩種線程定義方式都有這兩個問題:
- 無法獲取子線程的返回值
- run方法不可以拋出異常
為了解決這兩個問題,我們就需要用到Callable這個接口了。
6.2 線程同步:鎖對象
同步代碼依賴于一種簡單的可重入鎖。這種鎖易于使用,但有很多限制。java.util.concurrent.locks軟件包支持更復雜的鎖定習慣用法 。Lock對象的工作方式非常類似于同步代碼所使用的隱式鎖。與隱式鎖一樣,一次只能有一個線程擁有一個Lock對象。Lock對象還wait/notify通過其關聯的Condition對象支持一種機制 。
- reentrantLock.lock()
- reentrantLock.unlock()
6.3 線程池
線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建, 使用完畢不需要銷毀線程而是放回池中,從而減少創建和銷毀線程對象的開銷。
Java中創建和銷毀一個線程是比較昂貴的操作,需要系統調用。頻繁創建和銷毀線程會影響系統性能。Java 通過 Executors 提供四種線程池,分別為:
- newCachedThreadPool 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
- newFixedThreadPool 創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。
- newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
- newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務, 保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
6.4 并發集合:BLockingQueue
BlockingQueue實現被設計為主要用于生產者-消費者隊列,如果BlockingQueue是空的,從BlockingQueue取東西的操作將會被阻斷進入等待狀態,直到BlockingQueue進了東西才會被喚醒。同樣,如果BlockingQueue是滿的,任何試圖往里存東西的操作也會被阻斷進入等待狀態,直到BlockingQueue里有空間時才會被喚醒。
import java.util.concurrent.BlockingQueue; public class Consumer implements Runnable {BlockingQueue<Product> blockingQueue;public Consumer(BlockingQueue<Product> blockingQueue) {this.blockingQueue = blockingQueue;}@Overridepublic void run() {try {while (true) {Product product = blockingQueue.take();System.out.println("消費產品: " + product.getName());Thread.sleep(1000);}} catch (InterruptedException e) {e.printStackTrace();}} }6.5 靜態代理
真實對象和代理對象都要實現同一個接口,代理對象要代理真實角色。
好處:代理對象可以做很多真實對象做不了的事情,真實對象只專注做自己的事情,如婚慶公司和結婚人之間的關系
6.6 Lambda表達式
- Lambda 表達式,也可稱為閉包,它是推動 Java 8 發布的最重要新特性。
- Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。
- 使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。
Lambda表達式的重要特征:
- 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
- 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
- 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
- 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
Lambda 表達式的簡單例子:
// 1. 不需要參數,返回值為 5 () -> 5 // 2. 接收一個參數(數字類型),返回其2倍的值 x -> 2 * x // 3. 接受2個參數(數字),并返回他們的差值 (x, y) -> x – y // 4. 接收2個int型整數,返回他們的和 (int x, int y) -> x + y // 5. 接受一個 string 對象,并在控制臺打印,不返回任何值(看起來像是返回void) (String s) -> System.out.print(s)總結
以上是生活随笔為你收集整理的JavaSE(十一)——多线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaSE(十)——set和map集合
- 下一篇: JavaSE(十二)——AWT