1.線程是什么?
進程:每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1–n個線程。線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。
同一時刻運行多個程序的能力。每一個任務稱為一個線程??梢酝瑫r運行一個以上線程的程序稱為多線程程序。
JAVA編寫程序都運行在在Java虛擬機(JVM)中,在JVM的內部,程序的多任務是通過線程來實現的。每用java命令啟動一個java應用程序,就會啟動一個JVM進程。在同一個JVM進程中,有且只有一個進程,就是它自己。在這個JVM環境中,所有程序代碼的運行都是以線程來運行。
一般常見的Java應用程序都是單線程的。比如,用java命令運行一個最簡單的HelloWorld的Java應用程序時,就啟動了一個JVM進程,JVM找到程序程序的入口點main(),然后運行main()方法,這樣就產生了一個線程,這個線程稱之為主線程。當main方法結束后,主線程運行完成。JVM進程也隨即退出 。
對于一個進程中的多個線程來說,多個線程共享進程的內存塊,當有新的線程產生的時候,操作系統不分配新的內存,而是讓新線程共享原有的進程塊的內存。因此,線程間的通信很容易,速度也很快。不同的進程因為處于不同的內存塊,因此進程之間的通信相對困難。
線程分為兩類:用戶線程和守候線程。當所有用戶線程執行完畢后,JVM自動關閉。但是守候線程卻不獨立與JVM,守候線程一般是有操作系統或用戶自己創建的。
2.線程的生命周期
線程是一個動態執行的過程,它也有一個從產生到死亡的過程。
下圖顯示了一個線程完整的生命周期。
- 新建狀態:使用 new 關鍵字和 Thread 類或其子類建立一個線程對象后,該線程對象就處于新建狀態。它保持這個狀態直到程序 start() 這個線程。
- 就緒狀態:當線程對象調用了start()方法之后,該線程就進入就緒狀態。就緒狀態的線程處于就緒隊列中,要等待JVM里線程調度器的調度。
- 運行狀態:如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處于運行狀態。處于運行狀態的線程最為復雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
- 阻塞狀態:如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之后,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源后可以重新進入就緒狀態。可以分為三種:
- 等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。
- 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。
- 其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
- 死亡狀態:一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。
3.如何創建一個線程
Java 提供了三種創建線程的方法:
- 通過實現 Runnable 接口;
- 通過繼承 Thread 類本身;
- 通過 Callable 和 Future 創建線程。
通過實現 Runnable 接口來創建線程
創建一個線程,最簡單的方法是創建一個實現 Runnable 接口的類。
為了實現 Runnable,一個類只需要執行一個方法調用 run(),聲明如下:
下面是一個創建線程并開始讓它執行的實例:
package org.java.base.thread;public class RunnableDemo implements Runnable{@Overridepublic void run() {System.out.println(“我是線程”);}}
通過繼承Thread來創建線程
創建一個線程的第二種方法是創建一個新的類,該類繼承 Thread 類,然后創建一個該類的實例。
該方法盡管被列為一種多線程實現方式,但是本質上也是實現了 Runnable 接口的一個實例。
package org.java.base.thread;public class ThreadDemo extends Thread{@Overridepublic void run() {System.out.println(“我是線程”);}}
Thread 方法
下表列出了Thread類的一些重要方法:
序號方法描述1public void start()
使該線程開始執行;Java 虛擬機調用該線程的 run 方法。2public void run()
如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作并返回。3public final void setName(String name)
改變線程名稱,使之與參數 name 相同。4public final void setPriority(int priority)
更改線程的優先級。5public final void setDaemon(boolean on)
將該線程標記為守護線程或用戶線程。6public final void join(long millisec)
等待該線程終止的時間最長為 millis 毫秒。7public void interrupt()
中斷線程。8public final boolean isAlive()
測試線程是否處于活動狀態。
測試線程是否處于活動狀態。 上述方法是被Thread對象調用的。下面的方法是Thread類的靜態方法。
序號方法描述1public static void yield()
暫停當前正在執行的線程對象,并執行其他線程。2public static void sleep(long millisec)
在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。3public static boolean holdsLock(Object x)
當且僅當當前線程在指定的對象上保持監視器鎖時,才返回 true。4public static Thread currentThread()
返回對當前正在執行的線程對象的引用。5public static void dumpStack()
將當前線程的堆棧跟蹤打印至標準錯誤流。
通過 Callable 和 Future 創建線程
- 1. 創建 Callable 接口的實現類,并實現 call() 方法,該 call() 方法將作為線程執行體,并且有返回值。
- 2. 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
- 3. 使用 FutureTask 對象作為 Thread 對象的 target 創建并啟動新線程。
- 4. 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束后的返回值。
package org.java.base.thread;import java.util.concurrent.Callable;public class CallableThreadDemo implements Callable<Integer>{@Overridepublic Integer call() throws Exception {System.out.println(“我是一個Callable實現”);return 1;}}
- 創建線程的三種方式的對比
- 1. 采用實現 Runnable、Callable 接口的方式創建多線程時,線程類只是實現了 Runnable 接口或 Callable 接口,還可以繼承其他類。
- 2. 使用繼承 Thread 類的方式創建多線程時,編寫簡單,如果需要訪問當前線程,則無需使用 Thread.currentThread() 方法,直接使用 this 即可獲得當前線程。
有效利用多線程的關鍵是理解程序是并發執行而不是串行執行的。例如:程序中有兩個子系統需要并發執行,這時候就需要利用多線程編程。通過對多線程的使用,可以編寫出非常高效的程序。不過請注意,如果你創建太多的線程,程序執行的效率實際上是降低了,而不是提升了。請記住,上下文的切換開銷也很重要,如果你創建了太多的線程,CPU 花費在上下文的切換的時間將多于執行程序的時間!
4.線程通信
正常情況下,每個子線程完成各自的任務就可以結束了。不過有的時候,我們希望多個線程協同工作來完成某個任務,這時就涉及到了線程間通信了。
線程之間通信方式:
1.是通過共享變量,線程之間通過該變量進行協作通信;例如:多個線程共享同一個變量,要考慮并發的問題2.通過隊列(本質上也是線程間共享同一塊內存)來實現消費者和生產者的模式來進行通信;例如:異步發送郵件或者短信
5.線程同步
java允許多線程并發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),
將會導致數據不準確,相互之間產生沖突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,
從而保證了該變量的唯一性和準確性。
即有synchronized關鍵字修飾的方法。由于java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時, 內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處于阻塞狀態。
代碼如:
public synchronized void save(){}
注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類
6.線程死鎖
死鎖就是兩個或兩個以上的線程被無限的阻塞線程之間相互等待所需的資源”>死鎖就是兩個或兩個以上的線程被無限的阻塞,線程之間相互等待所需的資源。這種情況可能發生在當兩個線程嘗試獲取其他資源的鎖,而每個線程又陷入無線等待其他資源鎖的釋放,除非一個用戶的進程被終止。
線程死鎖可能發生在以下的情況:
- 當兩個線程相互調用Thread.join();
- 當兩個線程使用嵌套的同步塊時,一個線程占用了另一個線程的必需的鎖,互相等待時被阻塞,就有可能出現死鎖。
死鎖一般都是由于對共享資源的競爭所引起的。但對共享資源的競爭又不一定就會發生死鎖。
死鎖的發生必需滿足4個必要條件:
- 互斥
- 等待/持有
- 非搶占
- 形成等待環