多線程
- 程序、進程、線程:
- 程序Program:指令集,靜態概念。
- 進程Process:操作系統,調度程序,動態概念。
- 進程是程序的一次動態執行過程,佔用特定的地址空間
- 每個進程都是獨立的,由三部分組成:cpu,data,code
- 一個程序就是一個進程。
- 線程Thread:在進程內,多條執行路徑。(不同的執行路徑)
- 一個進程可以擁有多個並行的(concurrent)線程。
- 一個進程中的線程共享相同的內存單元/內存地址空間->可以訪問相同的變量和對象,而且它們從同一個堆中分配對象->通信、數據交換、同步操作。
- 會造成併發的問題。
- 線程和進程的區別:
- 進程:作爲資源分配的單位
- 線程:調度和執行的單位
- 包含關係:
- 沒有線程的進程,可以看做單線程的,如果一個進程內部擁有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的。
- 線程的基本概念
- 線程是一個程序內部的順序控制流。
- 線程和進程的區別
- 每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷。
- 線程可以看成時輕量級的進程,同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換的開銷小。
- 多進程: 在操作系統中能同時運行多個任務(程序)
- 多線程: 在同一應用程序中有多個順序流同時執行
- Java的線程是通過java.lang.Thread類來實現的。
- 擴充
- 一個進程是一個正運行的應用程序的實例 。它由兩個部分組成:一個是操作系統用來管理這個進程的內核對象。另一個是這個進程擁有的地址空間。從執行角度方面看,一個進程由一個或多個線程組成。
- 一個線程是一個執行單元,它控制CPU執行進程中某一段代碼段。
- 一個線程可以訪問這個進程中所有的地址空間和資源。
- 一個進程最少包括一個線程來執行代碼,這個線程又叫做主線程。
- 線程除了能夠訪問進程的資源外,每個線程還擁有自己的棧
- 線程的創建
- 第一種方式
- 繼承Thread類,重寫run方法。
- 第二種
- 實現Runnable接口,重寫run方法。
- 第一種方式
- 第二種方式代碼示例:
-
public class Demo2 { public static void main(String[] args) { //1、創建真是角色 Programme p1 = new Programme(); //2、創建代理角色+真實角色引用 Thread thread = new Thread(p1); //3、代理角色調用start方法開啓線程 thread.start(); for (int i = 0; i < 2000; i++) { System.err.println("一邊查文檔"); } } } /** * @author Johnny * @category 實現了Runnable接口 */ class Programme implements Runnable { public void run() { for (int i = 0; i < 2000; i++) { System.out.println("一邊寫代碼"); } } }
-
- 實現Runnable接口優點:
- 可以同時實現繼承。實現Runnable接口方式要通用一些。
- 避免單繼承。
- 方便共享資源,同一份資源,多個代理訪問。
- 線程的五個狀態
- 新生狀態:
- 用new關鍵字和Thread類或其子類建立一個線程對象後,訪問線程對象就處於新生狀態。處於新生狀態的線程有自己的內存空間,通過調用start方法進入就緒狀態(runnable)
- 就緒狀態:
- 處於就緒狀態的線程已經具備了運行條件,但還沒有分配到cpu,處於線程就緒隊列,等待系統爲其分配cpu。等待狀態並不是執行狀態。
- 運行狀態:
- 在運行狀態和線程執行自己的run方法中代碼,直到調用其他方法而終止或等待某資源而阻塞或完成任務而終止。
- 阻塞狀態:
- 處於運行狀態的線程在某些情況下,如執行了sleep(睡眠)方法,或等待I/O設備等資源,將讓出CPU並暫時停止自己的運行,進入阻塞狀態。
- 終止狀態:
- 終止狀態是線程生命週期中的最後一個階段。線程終止的原因有兩個,一個是正常運行的線程完成了它的全部工作;另一種是線程被強制執行的終止,如:通過執行stop或destory方法來終止一個線程(前者會產生異常,後者是強制終止不會釋放鎖,都不推薦)
- 新生狀態:
- 線程的常用方法:
- isAlive()
- 判斷線程是否處於活動狀態,即線程是否還未終止。
- getPriority()
- 獲得線程的優先級數值
- setPriority()
- 設置線程的優先級數值
- Thread.sleep()
- 將當前線程睡眠指定毫秒數
- join()
- 調用某線程的該方法,將當前線程與該線程“合併”,即等待該線程結束,再恢復當前線程的運行。
- yield()
- 讓出CPU,當前線程進入就緒隊列等待調度。
- wait()
- 當前線程進入對象的wait pool。
- notify()/notifyAll()
- 喚醒對象的wait pool中的一個/所有等待線程。
- static void sleep(long millis)
- 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。
- isAlive()
- interrupt
- 中斷線程的方法。
- 中斷線程同時拋出一個異常:InterruptedException。
- InterruptedException:該異常是編譯異常(也稱受檢查異常)。
- join()
- 實現了“整體插隊”的效果,將當前加入的線程執行完畢,然後繼續執行外面的線程。
- thread.Join把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行的線程。
- 比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。
- yield()
- yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。
- 結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
- 兩種線程模式:
- 協作式:一個線程保留對處理器的控制直到它自己決定放棄
- 速度快、代價低
- 用戶編程非常麻煩
- 搶先式:系統可以任意的從線程中奪回對CPU的控制權,再把控制權分給其它的線程 。
- 兩次切換之間的時間間隔就叫做時間片
- 效率不如協作式高 ,OS核心必須負責管理線程
- 簡化編程,而且使程序更加可靠
- 多數線程的調度是搶先式的。
- 協作式:一個線程保留對處理器的控制直到它自己決定放棄
- synchronized
- 由於同一個進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問衝突這個嚴重的問題。
- java語言提供了專門機制以解決這種衝突,有效的避免了同一個數據被多個線程同時訪問。
- 由於我們可以通過private關鍵字來保證數據對象只能被方法訪問,所以我們只需要針對方法提出一套機制,
- 這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊確保資源安全,線程安全。
- 關鍵字synchronized 來與對象的互斥鎖聯繫。當某個對象synchronized修飾時,表明該對象在任一時刻只能由一個線程訪問。
- Web12306線程安全代碼:
-
public class Web12306XCAQ implements Runnable{ private int num = 20;//共享資源 private boolean flag = true; @Override public void run() { while(flag){ method2(); } } public synchronized void method1(){ if(num<1){ flag=false; }else{ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } //說出當前線程的名稱,誰搶了第?張票 System.out.println( Thread.currentThread().getName()+"搶了第"+num--+"張票"); } } public void method2(){ synchronized (this) { if(num<1){ flag=false; }else{ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } //說出當前線程的名稱,誰搶了第?張票 System.out.println( Thread.currentThread().getName()+"搶了第"+num--+"張票"); } } } }
-
- 線程死鎖
- 過多的同步容易造成死鎖
- 一個線程中鎖定了另一個線程需要的對象,而另一個線程鎖定了這個線程需要的對象。
- 也就是說:兩個線程互相鎖定對方所需要的對象。
- synchronized總結
- 無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是鎖在了對象上,而不是把一段代碼或函數當作鎖,而且同步方法很可能還會被其他線程的對象訪問。
- 每個對象只有一個鎖(lock)與之相關聯。
- 實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。
- 搞清楚synchronized鎖定的是哪個對象,就能幫助我們設計更安全的多線程程序。
- wait() :等待、釋放鎖
- notify()、notifyAll() :喚醒
- 他們三個都是在同步中使用
- 生產者繳費者模式,解決線程死鎖問題(是一個解決方案)
- 該問題的關鍵就是要保證生產者不會再緩衝區滿時加入數據,消費者也不會再緩衝區空時消耗數據。
- 通常常用的方法有信號燈法。
- Wait sleep區別
- 來源不同
- Sleep是Thread提供的方法
- Wait來自Object
- 代碼位置不同
- Wait需要寫在Synchronize語句塊裏面
- 是否釋放鎖定對象
- 調用wait方法,釋放鎖定該對象
- Sleep時別的線程也不可以訪問鎖定對象
- 來源不同