并行與并發的區別
- 并行是多核 CPU 上的多任務處理,多個任務在同一時間真正的同時執行
- 并發是單核 CPU 上的多任務處理,多個任務在同一時間段內交替執行,通過時間片輪轉實現交替執行,用于解決 IO 密集型任務的瓶頸
線程的創建方式
Thread 的構造方法,可以在創建線程的時候為線程起名字
- 第一種方法:繼承 Thread 類
- 第一步:編寫一個類繼承 Thread
- 第二步:重寫 run 方法
- 第三步:new 線程對象
- 第四步:調用線程對象的 start 方法,啟動線程
一定調用的是 start 方法,不是 run 方法。start 方法的作用就是啟動一個線程,線程啟動完成該方法就結束。
public class MyThread {public static void main(String[] args) {NewThread nt = new NewThread();// 啟動 start 方法啟動線程,而不是 run 方法nt.start();for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}}
}class NewThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}}
}
對比一下 run 方法與 start 方法的內存圖
調用 run 方法的內存圖
調用 run 方法并沒有啟動新線程,代碼都是在 main 方法中執行的,內存只有一個主線程的棧,因此必須 run 方法中的代碼執行玩,才能執行后續的代碼
調用 start 方法的內存圖
調用 start 方法會啟動一個新線程,內存會分配一個新的棧空間給新線程(分配完成start方法就結束了,main 方法中的代碼繼續向下執行),新線程的代碼在新的棧空間執行,main 方法的代碼在 main 方法的棧空間執行,兩個線程搶奪 CPU 時間片交替執行。
2. 第二種方法:實現 Runnable 接口
- 第一步:編寫一個類實現 Runnable 接口
- 第二步:實現接口中的 run 方法
- 第三步:new 線程對象
使用Thread的帶有 Runnable 參數的構造方法創建對象
- 第四步:調用線程對象的 start 方法,啟動線程
推薦使用這種而不是第一種,因為第一種方式使用繼承,而Java只能單繼承,因此失去了繼承其他類的能力,而 實現 Runnable 接口則還能繼承其他類
- 實現 Callable 接口
- 第一步:編寫一個類實現 Callable 接口
- 第二步:實現接口中的 run 方法
- 第三步:new 線程對象
- 第四步:調用線程對象的 start 方法,啟動線程
線程常用的三個方法
final String getName()
:返回此線程的名稱。final void setName(String name)
:將此線程的名稱更改為等于參數 name 。static Thread currentThread()
:返回對當前正在執行的線程對象的引用。
線程七個生命周期
- NEW:新建狀態
當線程被創建但尚未開始運行時,它處于新建狀態。在這個階段,線程對象被實例化,但尚未調用 start() 方法。 - RUNNABLE:可運行狀態
- 就緒狀態
一旦調用了 start() 方法,線程進入就緒狀態。在這個狀態下,線程準備好運行,并等待線程調度程序的分配。此狀態并不一定代表線程正在運行,可能會處于等待獲取CPU時間片的狀態。 - 運行狀態
線程獲得CPU資源并開始執行其任務時,線程進入運行狀態。在這個狀態下,線程執行其代碼。
- 就緒狀態
- BLOCKED:阻塞狀態
當線程嘗試獲取一個已經被其他線程持有的鎖時,它會進入阻塞狀態。在這個狀態下,線程無法繼續執行,直到它獲得所需的鎖。 - WAITING:等待狀態
如果線程調用 wait()、join() 或 LockSupport.park() 方法,它會進入等待狀態。在這種狀態下,線程會等待其他線程通知或喚醒它。 - TIMED_WAITING:超時等待狀態
線程在等待的同時設置了時間限制(例如,調用 sleep(milliseconds) 或 wait(milliseconds)),將進入超時等待狀態。如果在超時時間到達之前線程未被喚醒,則該線程會返回到就緒狀態。 - TERMINATED:終止/死亡狀態
當線程的 run() 方法執行完畢或者因異常終止時,線程進入終止狀態。在這個狀態下,線程完成了它的生命周期,無法重新啟動。
線程常用的調度方法
- start()
用于啟動線程,使其進入就緒狀態。 - sleep(long millis)
使當前正在執行的線程暫停指定的時間,被調用的線程會進入阻塞狀態
,在指定的毫秒數后,線程會回到就緒狀態。 - yield()
暫時讓出當前線程的執行權,該方法提示線程調度器允許其他同等優先級的線程獲得執行時間。
并不保證在調用后立即釋放控制權。
讓位后的線程進入就緒狀態,并不會阻塞。 - join()
等待一個線程完成,如果線程 A 調用線程 B 的 join() 方法,線程 A 會阻塞,直到線程 B 執行完畢并終止。
join(long millis) 方法也可以指定時間,指的是加入 A 線程的時間或者說阻塞 A 線程的時間,時間一到就退出。如果在指定的 millis 時間內,B 線程結束了,被阻塞的 A 線程也會結束阻塞狀態。 - interrupt()
發送一個中斷信號給線程。如果線程正處于等待、睡眠或阻塞狀態,將會拋出InterruptedException。拋出異常就會導致線程退出等待、睡眠或阻塞狀態,從而達到中斷的目的,利用了異常的機制。 - setPriority(int newPriority)
設置線程的優先級。線程的優先級是一個整數值,范圍從1(最低)到10(最高)。這并不保證線程會按優先級執行,但可以指示調度器的優先級。 - wait() 和 notify()
用于線程間的通信和協作。wait() 使線程在對象監視器上等待,直到其他線程調用 notify() 或 notifyAll() 來喚醒它。
如何強制結束一個線程
Thread.stop() 方法可以強制結束線程,但是在 Java1.2 之后就被棄用了,因為使用 stop() 方法會導致線程立即終止,這可能導致鎖未解鎖、文件未正確關閉等情況,因此強烈不推薦。更好的辦法是使用標志位或者 interrupt() 方法。
- 設置標志位方法
通過使用一個共享的標志位來控制線程的生命周期是推薦的做法。主線程可以通過設置標志位來通知子線程應停止執行。class CustomThread extends Thread { private volatile boolean running = true; // 使用 volatile 關鍵字確保可見性 public void run() { while (running) { // 執行任務 System.out.println("Thread is running..."); try { Thread.sleep(500); // 模擬工作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢復中斷狀態 } } System.out.println("Thread is stopping..."); } public void stopRunning() { running = false; // 設置標志位 } } public class Main { public static void main(String[] args) throws InterruptedException { CustomThread thread = new CustomThread(); thread.start(); Thread.sleep(2000); // 讓線程運行2秒 thread.stopRunning(); // 請求線程停止 thread.join(); // 等待線程結束 System.out.println("Main thread finished."); } }
- 使用 interrupt() 方法
在Java中,interrupt() 方法可以用于中斷一個線程。線程在被中斷時可以選擇捕捉異常或者檢查中斷狀態,從而優雅地結束自己。class InterruptibleThread extends Thread { public void run() { try { while (!Thread.currentThread().isInterrupted()) { // 執行任務 System.out.println("Thread is running..."); Thread.sleep(500); // 模擬工作 } } catch (InterruptedException e) { // 捕捉到中斷異常,線程可以選擇停止 System.out.println("Thread was interrupted."); Thread.currentThread().interrupt(); // 重新設置中斷狀態 } System.out.println("Thread is stopping..."); } } public class Main { public static void main(String[] args) throws InterruptedException { InterruptibleThread thread = new InterruptibleThread(); thread.start(); Thread.sleep(2000); // 讓線程運行2秒 thread.interrupt(); // 中斷線程 thread.join(); // 等待線程結束 System.out.println("Main thread finished."); } }
守護線程
在 Java 中,線程被分為兩大類,一類是用戶線程,一類是守護線程。
在 JVM 中,有一個隱藏的守護線程就是 GC 線程
守護線程的特點:
- 后臺運行: 守護線程通常是后臺執行的,用于執行一些輔助任務,比如垃圾回收、線程池中的工作線程等。
- 生命周期受限: JVM 會在所有非守護線程結束后自動結束守護線程。如果沒有非守護線程在運行,JVM會退出。
- 優先級: 守護線程的優先級與普通線程相同,但它們的作用通常是協助非守護線程。
如何創建守護線程
三個步驟創建守護線程:
- 創建一個線程實例
- 調用
setDaemon(true)
方法,將該線程設置為守護線程 - 啟動線程
class DaemonThread extends Thread { @Override public void run() { while (true) { System.out.println("Daemon thread is running..."); try { Thread.sleep(1000); // 模擬一些工作 } catch (InterruptedException e) { System.out.println("Daemon thread interrupted."); } } }
} public class Main { public static void main(String[] args) { Thread daemonThread = new DaemonThread(); daemonThread.setDaemon(true); // 設置為守護線程 daemonThread.start(); try { Thread.sleep(3000); // 主線程睡眠3秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main thread is ending..."); // 主線程結束,Daemon線程會隨之結束 }
}
定時任務
在Java中,Timer 是一個用于調度任務的工具類,允許開發者在指定的時間間隔內重復執行任務或在特定的時間點執行任務。Timer 類通常與 TimerTask 類一起使用,其功能足夠簡單,適合于許多基本的定時任務需求。
Timer: 一個定時器,負責調度任務。
TimerTask: 一個抽象類,所有需要被調度的任務都需要繼承這個類并重寫 run() 方法。
創建定時任務
使用 Timer 和 TimerTask 來創建定時任務的基本步驟如下:
- 創建一個 Timer 實例。
- 創建一個繼承自 TimerTask 的類,并實現 run() 方法。
- 使用 Timer 的 schedule() 或 scheduleAtFixedRate() 方法將任務和執行時間關聯。
常用方法
- schedule(TimerTask task, long delay): 在指定的延遲后調度任務。
- schedule(TimerTask task, Date time): 在指定的時間執行任務。
- schedule(TimerTask task, long delay, long period): 設定任務在指定的延遲后每隔一個時間段再次執行。
- scheduleAtFixedRate(TimerTask task, long delay, long period): 類似于 schedule,但是以固定率運行,適用于需要固定間隔執行的任務。
import java.util.Timer;
import java.util.TimerTask; public class TimerExample { public static void main(String[] args) { Timer timer = new Timer(); // 創建定時器 TimerTask task = new TimerTask() { @Override public void run() { System.out.println("Task executed at: " + System.currentTimeMillis()); } }; // 在延遲 1 秒后執行任務,每隔 2 秒重復執行 timer.scheduleAtFixedRate(task, 1000, 2000); // 主線程睡眠 10 秒,以便觀察輸出 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } // 取消定時器 timer.cancel(); System.out.println("Timer canceled."); }
}
注意事項
- 單線程中: Timer 是單線程的,如果一個 TimerTask 執行時間超過下一個任務的調度時間,后續的任務會被延遲執行。
- 異常處理: 如果 TimerTask 中的代碼拋出未處理的異常,Timer 將停止執行所有后續任務。應確保 run() 方法內的代碼是異常安全的。
- 使用 ScheduledExecutorService: 對于更復雜的定時任務需求(例如線程池,多線程調度等),建議使用 ScheduledExecutorService,它提供了更強大的功能和靈活性。
import java.util.concurrent.*; public class ScheduledExecutorExample { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Runnable task = () -> { System.out.println("Task executed at: " + System.currentTimeMillis()); }; // 在延遲 1 秒后執行,每隔 2 秒重復執行 scheduler.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS); // 主線程睡眠 10 秒,以便觀察輸出 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } // 關閉調度器 scheduler.shutdown(); System.out.println("Scheduler shut down."); } }
- 開發中一般不使用 Timer ,有一些更好的框架可以設置定時任務。
線程優先級
- 線程是可以設置優先級的,優先級高的,獲得CPU時間片的概率會高一些
- JVM 采用的是搶占式調度模式。誰的優先級高,獲取 CPU 的概率就會高
- 默認情況下,一個線程的優先級是 5
- 線程優先級最低是 1,最高是 10
- Thread 類的字段屬性
可以通過 MAX_PRIORITY 和 MIN_PRIORITY,設置最高最低優先級
線程安全
什么情況下需要考慮線程安全問題?
- 多線程的并發環境下
- 有共享的數據
- 共享數據涉及到修改操作
一般情況下,局部變量不存在線程安全問題,實例變量和靜態變量可能存在線程安全問題。因為局部遍歷存儲在棧中,實例變量和靜態變量存儲在堆中,棧每個線程使用自己的,而堆是多線程共享的。