寫在前面
博主在之前寫了很多關于并發編程深入理解的系列文章,有博友反饋說對博主的文章表示非常有收獲但是對作者文章的某些基礎描述有些模糊,所以博主再根據最能接觸到的基礎,為這類博友進行掃盲!當然,后續仍然會接著進行創作且更傾向于實戰Demo,希望令友友們有期待更希望有收獲!
>>>線程簡介
什么是線程
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位 。打個比方,如果把進程看作是一個工廠,那么線程就是工廠里的一條生產線。一個進程中可以并發多個線程,每條線程并行執行不同的任務,就如同一個工廠中可以有多條生產線同時工作,各自生產不同的產品。
在 Java 中,線程擁有自己獨立的運行棧和程序計數器,這保證了線程在執行時的獨立性。但同一進程中的多個線程會共享進程的堆內存和方法區內存,就像同屬一個工廠的生產線共享原材料倉庫和生產工藝一樣。例如,在一個 Java Web 應用程序中,會有多個線程同時處理不同用戶的請求,這些線程共享應用程序的內存空間和資源。
為什么要使用多線程
- 提高程序執行效率:相較于進程,線程的創建和切換開銷更小。進程創建時需要分配獨立的內存空間、文件描述符等資源,而線程創建時僅需分配少量的棧空間和寄存器等資源,切換時也只需保存和恢復少量的寄存器內容,因此線程的創建和切換速度更快,能有效減少系統開銷,提高程序執行效率。以一個文件處理程序為例,若使用單線程,在讀取文件內容時,線程會處于阻塞狀態,CPU 資源被閑置;而采用多線程,可在一個線程讀取文件時,另一個線程對已讀取的數據進行處理,從而充分利用 CPU 資源,提升程序整體執行效率。
- 充分利用多處理器資源:在多核處理器環境下,多線程能使程序充分利用多個處理器核心。每個線程可被分配到不同的處理器核心上并行執行,如同多個工人同時在不同的生產線上工作,極大地提高了程序的并行處理能力。例如,在進行大數據分析時,可將數據處理任務分解為多個子任務,每個子任務由一個線程負責,并在不同的處理器核心上執行,從而加快數據分析速度。
- 方便數據共享:同一進程內的多個線程共享進程的內存空間和資源,數據共享變得非常便捷。線程間可以直接訪問共享內存中的數據,無需像進程間通信那樣借助復雜的機制。比如在一個圖形繪制程序中,負責繪制圖形的線程和負責處理用戶輸入的線程可以共享圖形數據,方便實時更新圖形顯示。
- 提高程序響應性:在一些需要實時響應用戶操作的應用程序中,多線程可使程序在處理耗時任務時,依然能快速響應用戶的其他操作。例如,在一個音樂播放器應用中,主線程負責處理用戶的播放、暫停、切換歌曲等操作,而播放音樂的任務則由一個單獨的線程完成。這樣,當用戶在播放音樂時進行其他操作,如調整音量,主線程能及時響應,不會因為音樂播放的耗時操作而出現卡頓。
線程優先級
在 Java 中,每個線程都有一個優先級,它是一個整數,范圍從 1 到 10。優先級越高的線程,在競爭 CPU 資源時越有可能被優先調度執行,但這并不意味著低優先級的線程就不會被執行,只是執行的機會相對較少。線程優先級的默認值為 5。
Java 的Thread類中定義了三個常量來表示線程優先級:
- Thread.MIN_PRIORITY:表示最低優先級,值為 1。
- Thread.NORM_PRIORITY:表示普通優先級,值為 5。
- Thread.MAX_PRIORITY:表示最高優先級,值為 10。
下面通過一個簡單的代碼示例來展示線程優先級的設置和使用:
public class ThreadPriorityDemo {public static void main(String[] args) {// 創建線程1并設置最低優先級Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("線程1,優先級:" + Thread.currentThread().getPriority());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});thread1.setPriority(Thread.MIN_PRIORITY);// 創建線程2并設置最高優先級Thread thread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("線程2,優先級:" + Thread.currentThread().getPriority());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});thread2.setPriority(Thread.MAX_PRIORITY);// 啟動兩個線程thread1.start();thread2.start();}
}
在上述代碼中,創建了兩個線程thread1和thread2,分別設置它們的優先級為最低和最高。運行程序后,可以觀察到線程 2 輸出的次數可能會比線程 1 多,這體現了優先級對線程調度的影響。但由于線程調度的不確定性,在不同的運行環境和次數下,結果可能會有所不同。
線程的狀態
在 Java 中,線程共有六種狀態,這些狀態反映了線程在其生命周期內的不同運行情況,它們定義在Thread類的State枚舉中,通過getState()方法可以獲取線程當前的狀態。
1.新建(NEW):當使用new關鍵字創建一個線程對象后,線程就處于新建狀態。此時線程還沒有開始運行,僅僅是一個對象實例,系統沒有為其分配運行所需的資源。例如:
Thread thread = new Thread();System.out.println(thread.getState()); // 輸出:NEW
2.可運行(RUNNABLE):調用線程的start()方法后,線程進入可運行狀態。處于此狀態的線程位于可運行線程池中,等待線程調度器選中并分配 CPU 資源。一旦獲得 CPU 時間片,線程就會進入運行中狀態。可運行狀態實際上包含了就緒(ready)和運行中(running)兩種子狀態,在 Java 中統一用RUNNABLE表示。例如:
Thread thread = new Thread(() -> {// 線程任務邏輯
});
thread.start();
System.out.println(thread.getState()); // 輸出:RUNNABLE
3.終結(TERMINATED):當線程的run()方法執行完畢,或者因異常退出run()方法時,線程就進入終結狀態,此時線程的生命周期結束,不再具備運行能力。例如:
Thread thread = new Thread(() -> {// 線程任務邏輯
});thread.start();while (thread.isAlive()) {// 等待線程執行完成
}System.out.println(thread.getState()); // 輸出結果:TERMINATED
4.阻塞(BLOCKED):當線程試圖獲取一個被其他線程持有的鎖時,如果獲取失敗,線程就會進入阻塞狀態。在阻塞狀態下,線程不會被分配 CPU 執行時間,直到它獲得鎖。例如:
public class BlockedState {private static final Object lock = new Object();public static void main(String[] args) {// 線程1獲取鎖并休眠Thread thread1 = new Thread(() -> {synchronized (lock) {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});// 線程2嘗試獲取已被占用的鎖Thread thread2 = new Thread(() -> {synchronized (lock) {// 等待鎖釋放}});thread1.start();// 確保線程1先獲取鎖try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}thread2.start();System.out.println("線程2的狀態:" + thread2.getState()); // 輸出:BLOCKED}
}
5.等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(如通知或中斷)才能繼續執行。例如,調用Object類的wait()方法、Thread類的join()方法等,會使線程進入等待狀態。處于等待狀態的線程不會被分配 CPU 執行時間,直到被喚醒。例如:
public class WaitingState {private static final Object lock = new Object();public static void main(String[] args) {// 創建等待線程Thread waitingThread = new Thread(() -> {synchronized (lock) {try {lock.wait();System.out.println("等待線程被喚醒");} catch (InterruptedException e) {e.printStackTrace();}}});waitingThread.start();// 主線程短暫休眠try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 創建通知線程new Thread(() -> {synchronized (lock) {lock.notify();System.out.println("已發送喚醒通知");}}).start();}
}
6.有時限的等待(TIMED_WAITING):該狀態與等待狀態類似,但線程會在指定的時間后自動返回,而無需其他線程的通知。例如,調用Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)等方法時,線程會進入有時限的等待狀態。例如:
public class TimedWaitingState {public static void main(String[] args) {Thread thread = new Thread(() -> {try {Thread.sleep(2000); // 線程休眠2秒System.out.println("線程休眠結束");} catch (InterruptedException e) {e.printStackTrace();}});thread.start(); // 啟動線程try {Thread.sleep(100); // 主線程短暫休眠} catch (InterruptedException e) {e.printStackTrace();}System.out.println("線程狀態:" + thread.getState()); // 輸出TIMED_WAITING狀態}
}
Daemon 線程
Daemon 線程,即守護線程,是一種特殊的線程。它的作用是為其他線程的運行提供服務,就像一個默默在后臺工作的助手。守護線程與用戶線程相對,用戶線程用于執行具體的業務邏輯,而守護線程則在后臺執行一些輔助性的任務,如垃圾回收線程就是一個典型的守護線程,它負責回收不再使用的內存空間,保證程序的內存使用效率。
守護線程和用戶線程的主要區別在于,當 JVM 中所有的用戶線程都結束時,無論守護線程是否完成任務,JVM 都會直接退出,而不會等待守護線程執行完畢。這是因為守護線程本身就是為用戶線程服務的,當用戶線程都不存在了,守護線程也就沒有存在的必要了。
在 Java 中,可以通過setDaemon(true)方法將一個線程設置為守護線程,但這個設置必須在線程啟動之前進行,否則會拋出IllegalThreadStateException異常。例如:
public class DaemonThreadDemo {public static void main(String[] args) {// 創建并啟動守護線程Thread daemonThread = new Thread(() -> {while (true) {try {Thread.sleep(1000);System.out.println("守護線程正在運行");} catch (InterruptedException e) {e.printStackTrace();}}});daemonThread.setDaemon(true);daemonThread.start();// 主線程休眠3秒try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主線程結束");}
}
在上述代碼中,創建了一個守護線程daemonThread,并將其設置為守護線程。主線程睡眠 3 秒后結束,此時盡管守護線程還在運行,但由于所有用戶線程(這里的主線程是用戶線程)都已結束,JVM 會直接退出,守護線程也隨之結束。
>>>啟動和終止線程
構造線程
在 Java 中,構造線程主要有兩種方式:繼承Thread類和實現Runnable接口。這兩種方式各有特點,適用于不同的場景。
- 繼承Thread類:通過繼承Thread類,并重寫其run()方法來定義線程的執行邏輯。這種方式的優點是代碼實現簡單直觀,直接使用Thread類的方法,無需額外的對象來代理線程執行。例如:
public class MyThread extends Thread {@Overridepublic void run() {// 線程任務邏輯System.out.println("Thread running by extending Thread class");}
}
在上述代碼中,MyThread類繼承自Thread類,并重寫了run()方法,在run()方法中定義了線程要執行的任務。
- 實現Runnable接口:實現Runnable接口,將線程的執行邏輯封裝在run()方法中,然后將實現了Runnable接口的對象作為參數傳遞給Thread類的構造函數來創建線程。這種方式的優勢在于,一個類可以在繼承其他類的同時實現Runnable接口,避免了 Java 單繼承的限制,并且更適合多個線程共享同一個資源的場景。例如:
public class MyRunnable implements Runnable {@Overridepublic void run() {// 線程執行任務System.out.println("Runnable接口實現的線程正在運行");}
}
使用時,可以這樣創建線程:
public class Main {public static void main(String[] args) {// 創建Runnable實現類實例MyRunnable task = new MyRunnable();// 創建并啟動線程Thread worker = new Thread(task);worker.start();}
}
在這個例子中,MyRunnable類實現了Runnable接口,然后通過Thread類的構造函數將MyRunnable對象包裝成一個線程。
啟動線程
當我們構造好線程對象后,需要調用start()方法來啟動線程。調用start()方法后,線程并不會立即開始執行,而是進入就緒狀態,被納入線程調度器的管理范圍,等待 CPU 調度。一旦獲得 CPU 時間片,線程就會執行其run()方法中的代碼。
start()方法的作用是通知 Java 虛擬機,該線程已經準備好,可以被調度執行了。它會觸發一系列底層操作,包括創建操作系統級別的線程、分配資源等。例如:
Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("線程正在運行:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
});
thread.start();
在上述代碼中,創建了一個線程對象thread,并調用start()方法啟動線程。線程啟動后,會執行run()方法中的循環,每隔 100 毫秒輸出一次信息。
需要注意的是,不能對同一個線程對象多次調用start()方法,否則會拋出IllegalThreadStateException異常。因為start()方法的設計規定,一個線程只能啟動一次,多次調用會導致線程生命周期管理的混亂。另外,不要直接調用線程的run()方法,雖然調用run()方法也會執行線程中的代碼,但它不會啟動新線程,只是在當前線程中執行run()方法的邏輯,這與通過start()方法啟動線程的效果完全不同。
理解過期的 suspend ()、resume () 和 stop ()
在早期的 Java 版本中,Thread類提供了suspend()、resume()和stop()方法來控制線程的執行,但這些方法現在已經被標記為過期,不再推薦使用,主要原因如下:
- suspend()方法:該方法用于暫停線程的執行,但它存在嚴重的問題。當一個線程調用suspend()方法后,線程會暫停執行,但它并不會釋放已經持有的鎖資源。這可能導致其他線程在等待獲取這些鎖時被阻塞,從而引發死鎖問題。例如:
public class SuspendDeadlock {private static final Object lock = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lock) {System.out.println("線程1獲得鎖");Thread.currentThread().suspend(); // 暫停當前線程System.out.println("線程1恢復執行");}});Thread thread2 = new Thread(() -> {synchronized (lock) {System.out.println("線程2獲得鎖");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}thread1.resume(); // 嘗試恢復線程1System.out.println("已發送恢復信號");}});thread1.start();thread2.start();}
}
在上述代碼中,線程 1 獲取鎖后調用suspend()方法暫停自己,線程 2 隨后嘗試獲取鎖并恢復線程 1,但由于線程 1 持有鎖,線程 2 無法獲取鎖,從而導致死鎖。
- resume()方法:resume()方法用于恢復被suspend()暫停的線程。它必須與suspend()方法成對使用,但由于suspend()方法存在死鎖風險,resume()方法也因此受到牽連,不再推薦使用。
- stop()方法:stop()方法用于立即終止線程的執行。它會使線程立即停止正在執行的任務,包括catch或finally語句中的代碼,并拋出ThreadDeath異常。這種強制終止線程的方式非常危險,因為它不會保證線程的資源能夠正常釋放,可能導致數據不一致、文件未關閉、數據庫連接未釋放等問題。例如,在一個對文件進行讀寫操作的線程中,如果使用stop()方法終止線程,可能會導致文件數據損壞或丟失。
安全地終止線程
為了安全地終止線程,通常不建議使用上述過期的方法,而是采用更溫和、安全的方式,例如使用標志位來控制線程的終止。這種方式的核心思想是在線程內部定義一個標志變量,通過修改這個標志變量的值來通知線程何時停止執行。
- 使用自定義標志位:在run()方法中,通過一個while循環和一個標志位來控制線程的執行。當標志位被設置為true時,while循環結束,線程正常退出。例如:
public class SafeThreadTermination {private static class MyTask implements Runnable {private volatile boolean stopRequested = false;@Overridepublic void run() {while (!stopRequested) {// 執行線程任務System.out.println("Thread is running");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread terminated");}public void requestStop() {stopRequested = true;}}public static void main(String[] args) {MyTask task = new MyTask();Thread workerThread = new Thread(task);workerThread.start();try {// 主線程等待3秒Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}// 請求停止工作線程task.requestStop();}
}
在上述代碼中,MyTask類實現了Runnable接口,在run()方法中通過while (!stopFlag)循環來判斷是否繼續執行任務。stop()方法用于將stopFlag設置為true,從而終止線程。
2. 使用interrupt()方法:interrupt()方法用于中斷線程。它并不會立即終止線程,而是設置線程的中斷標志。當線程處于阻塞狀態(如調用sleep()、wait()等方法)時,調用interrupt()方法會使線程拋出InterruptedException異常,從而有機會在捕獲異常后進行相應的處理,如終止線程。例如:
public class InterruptThread {public static void main(String[] args) {// 創建并啟動新線程Thread thread = new Thread(() -> {// 檢查線程中斷狀態while (!Thread.currentThread().isInterrupted()) {try {System.out.println("線程正在執行任務");Thread.sleep(1000); // 暫停1秒} catch (InterruptedException e) {// 恢復中斷狀態并退出循環Thread.currentThread().interrupt();System.out.println("接收到中斷信號,準備終止線程");break;}}});thread.start();// 主線程休眠3秒后中斷工作線程try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt();}
}
在這個例子中,線程在while循環中不斷檢查自身的中斷標志。當調用interrupt()方法后,線程的中斷標志被設置,由于線程處于睡眠狀態,會拋出InterruptedException異常。在捕獲異常后,重新設置中斷標志(這是一種常見的處理方式,以確保上層代碼能夠正確處理中斷),并跳出循環,從而安全地終止線程。
>>>線程間通信
volatile 和 synchronized 關鍵字
在 Java 并發編程中,volatile和synchronized關鍵字是保證線程安全和線程間通信的重要手段,它們有著不同的作用和特性。
- volatile關鍵字:主要有兩個作用,一是保證可見性,二是禁止指令重排。當一個變量被volatile修飾后,任何線程對它的修改都會立即被其他線程看到,這是因為volatile修飾的變量在寫操作時,會將修改后的值立即刷新到主內存,讀操作時會直接從主內存讀取,而不是從線程的工作內存中讀取舊值。例如:
public class VolatileExample {// 使用volatile修飾確保多線程環境下的可見性private volatile int value;// 設置值的方法public void setValue(int value) {this.value = value;}// 獲取值的方法public int getValue() {return value;}
}
在上述代碼中,value變量被volatile修飾,當一個線程調用setValue方法修改value的值時,其他線程調用getValue方法能立即獲取到最新的值。同時,volatile還能禁止指令重排,確保程序按照代碼編寫的順序執行,避免在多線程環境下由于指令重排導致的并發問題。比如在雙重檢查鎖定(DCL)實現單例模式時,如果不使用volatile修飾單例對象,可能會因為指令重排導致其他線程獲取到未初始化的單例對象。
- synchronized關鍵字:主要用于實現同步互斥,保證同一時刻只有一個線程能夠進入被synchronized修飾的代碼塊或方法,從而避免多線程對共享資源的競爭訪問。它可以修飾方法或代碼塊。當修飾方法時,整個方法都是同步的;當修飾代碼塊時,只有代碼塊中的內容是同步的。例如:
public class SynchronizedExample {private int count;// 線程安全的計數器遞增方法public synchronized void increment() {count++;}// 獲取當前計數值public int getCount() {return count;}
}
在這個例子中,increment方法被synchronized修飾,當一個線程調用increment方法時,其他線程必須等待該線程執行完increment方法,釋放鎖后才能進入,從而保證了count變量的操作是線程安全的。
volatile和synchronized的主要區別在于:volatile主要用于保證變量的可見性和禁止指令重排,它不會對代碼塊或方法進行加鎖,不能保證原子性操作;而synchronized通過加鎖機制實現同步互斥,既能保證可見性,也能保證原子性,但會帶來一定的性能開銷,因為加鎖和解鎖操作涉及到線程狀態的切換和資源的競爭。
等待 / 通知機制
在 Java 中,線程間的等待 / 通知機制是通過Object類的wait()、notify()和notifyAll()方法來實現的,這三個方法用于協調多個線程對共享資源的訪問,實現線程間的通信和協作。
- wait()方法:當一個線程調用對象的wait()方法后,該線程會釋放對象的鎖,并進入等待狀態,直到被其他線程調用notify()或notifyAll()方法喚醒,或者等待時間超時(如果使用帶超時參數的wait(long timeout)方法)。wait()方法必須在synchronized塊中調用,否則會拋出IllegalMonitorStateException異常。例如:
public class WaitNotifyExample {private static final Object lock = new Object();public static void main(String[] args) {Thread waitingThread = new Thread(() -> {synchronized (lock) {try {System.out.println("等待線程開始執行");lock.wait();System.out.println("等待線程被喚醒");} catch (InterruptedException e) {e.printStackTrace();}}});waitingThread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}Thread notifyingThread = new Thread(() -> {synchronized (lock) {System.out.println("通知線程準備喚醒");lock.notify();}});notifyingThread.start();}
}
在上述代碼中,線程 1 獲取鎖后調用lock.wait()方法,進入等待狀態并釋放鎖。線程 2 在 1 秒后獲取鎖,調用lock.notify()方法喚醒線程 1,線程 1 被喚醒后重新獲取鎖并繼續執行。
- notify()方法:隨機喚醒一個在該對象上等待的線程。當調用notify()方法時,會從等待該對象鎖的線程中選擇一個喚醒,被喚醒的線程會進入可運行狀態,等待獲取對象的鎖,一旦獲取到鎖,就會繼續執行wait()方法之后的代碼。
- notifyAll()方法:喚醒所有在該對象上等待的線程。與notify()方法不同,notifyAll()會將所有等待該對象鎖的線程都喚醒,這些線程都會進入可運行狀態,競爭對象的鎖,最終只有一個線程能獲取到鎖并繼續執行,其他線程則繼續等待。
需要注意的是,wait()、notify()和notifyAll()方法都依賴于對象的監視器(鎖),只有獲取了對象的鎖才能調用這些方法。并且,在使用等待 / 通知機制時,通常需要結合條件判斷來確保線程在合適的時機等待和喚醒,避免不必要的等待和競爭。例如在生產者 - 消費者模型中,生產者線程在緩沖區滿時調用wait()方法等待,消費者線程從緩沖區取走數據后調用notify()或notifyAll()方法喚醒生產者線程。
等待 / 通知的經典范式
等待 / 通知的經典范式是一種在多線程編程中常用的代碼結構,用于實現線程間的協作和同步,它遵循一定的模式和規范,能夠有效地避免死鎖和競態條件等問題。下面是一個典型的等待 / 通知經典范式的代碼示例:
public class WaitNotifyPattern {private static final Object lock = new Object();private static boolean condition = false;public static void main(String[] args) {Thread waiterThread = new Thread(() -> {synchronized (lock) {while (!condition) {try {System.out.println("等待線程進入等待狀態");lock.wait();System.out.println("等待線程收到通知");} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("等待線程執行后續操作");}});Thread notifierThread = new Thread(() -> {synchronized (lock) {condition = true;System.out.println("通知線程更新狀態并發送通知");lock.notify();}});waiterThread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}notifierThread.start();}
}
在這個范式中,包含以下幾個關鍵步驟:
- 條件判斷:在wait()方法前,使用while循環對條件進行判斷。這是因為wait()方法可能會被虛假喚醒(在沒有其他線程調用notify()或notifyAll()方法的情況下被喚醒),通過while循環可以確保只有在條件真正滿足時才繼續執行后續代碼,避免錯誤的執行。
- 等待:調用wait()方法使線程進入等待狀態,并釋放對象的鎖。這樣其他線程就有機會獲取鎖并修改條件。
- 通知:當條件滿足時,另一個線程獲取鎖,修改條件后調用notify()或notifyAll()方法通知等待的線程。被通知的線程會從wait()方法處返回,重新獲取鎖后繼續執行。
這種范式確保了線程間的協作和同步,使得在多線程環境下,線程能夠按照預期的順序和條件進行執行,有效地解決了線程間通信和資源競爭的問題 。
輸入 / 輸出流
在 Java 中,線程間可以通過輸入輸出流進行通信,這種方式常用于網絡編程和文件處理等場景。輸入輸出流提供了一種在不同線程之間傳遞數據的機制,通過將數據寫入輸出流,另一個線程可以從對應的輸入流中讀取數據,從而實現線程間的數據交換和通信。
以 Socket 通信為例,客戶端和服務器端的線程可以通過 Socket 的輸入輸出流進行數據傳輸。服務器端創建一個 ServerSocket 來監聽指定端口,當有客戶端連接時,服務器端會創建一個新的 Socket 與客戶端進行通信,并為這個 Socket 分配輸入輸出流。客戶端通過 Socket 連接到服務器端后,也能獲取到對應的輸入輸出流。例如:
- 服務器端代碼:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;public class Server {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8888)) {System.out.println("服務器啟動成功,正在等待客戶端連接...");Socket clientSocket = serverSocket.accept();System.out.println("客戶端連接成功");BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);new Thread(() -> {try {String inputLine;while ((inputLine = in.readLine()) != null) {System.out.println("收到客戶端消息:" + inputLine);out.println("服務器響應:" + inputLine);}} catch (IOException e) {e.printStackTrace();}}).start();} catch (IOException e) {e.printStackTrace();}}
}
- 客戶端代碼:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;public class Client {public static void main(String[] args) {try (Socket socket = new Socket("localhost", 8888)) {// 初始化輸入輸出流BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream(), true);BufferedReader consoleIn = new BufferedReader(new InputStreamReader(System.in));// 創建線程處理服務器消息new Thread(() -> {try {String serverResponse;while ((serverResponse = in.readLine()) != null) {System.out.println("服務器響應: " + serverResponse);}} catch (IOException e) {e.printStackTrace();}}).start();// 處理用戶輸入String userInput;while ((userInput = consoleIn.readLine()) != null) {out.println(userInput);}} catch (IOException e) {e.printStackTrace();}}
}
在上述代碼中,服務器端和客戶端分別創建了輸入輸出流來進行數據的讀寫。服務器端的線程負責讀取客戶端發送的消息并回復,客戶端的線程負責讀取服務器端的回復并顯示。通過這種方式,服務器端和客戶端的線程實現了基于輸入輸出流的通信。
thread.join () 的使用
在 Java 多線程編程中,thread.join()方法是一個非常有用的方法,它的作用是讓當前線程等待調用join()方法的線程執行完畢后再繼續執行。例如,在一個主線程中啟動了多個子線程,有時需要確保這些子線程都執行完成后,主線程再繼續執行后續的操作,這時就可以使用join()方法。
join()方法的工作原理是:當一個線程 A 調用另一個線程 B 的join()方法時,線程 A 會進入等待狀態,直到線程 B 執行完畢或者等待超時(如果使用帶超時參數的join(long millis)方法)。在等待過程中,線程 A 會釋放 CPU 資源,不會占用 CPU 時間片,直到線程 B 執行結束或者超時時間到達,線程 A 才會重新進入可運行狀態,繼續執行后續的代碼。例如:
public class JoinExample {public static void main(String[] args) {// 創建并啟動線程1Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("線程1運行中:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});// 創建并啟動線程2Thread thread2 = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("線程2運行中:" + i);try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}});thread1.start();thread2.start();// 等待所有線程執行完成try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主線程繼續執行:所有子線程已完成");}
}
在上述代碼中,主線程啟動了thread1和thread2兩個子線程,然后調用thread1.join()和thread2.join()方法,主線程會等待thread1和thread2執行完畢后才會繼續執行最后一行輸出語句。通過join()方法,實現了主線程與子線程之間的協作,確保了程序按照預期的順序執行 。
ThreadLocal 的使用
ThreadLocal是 Java 中一個用于線程本地存儲的類,它為每個線程提供了獨立的變量副本,使得每個線程都可以獨立地訪問和修改自己的變量副本,而不會影響其他線程的變量副本。這在多線程編程中非常有用,可以有效地避免線程安全問題,特別是在一些需要在多個方法之間傳遞線程特定數據的場景。
ThreadLocal的工作原理是:每個線程都有一個ThreadLocalMap對象,當線程通過ThreadLocal對象的get()方法獲取變量時,實際上是從該線程的ThreadLocalMap中獲取對應的值;當通過set()方法設置變量時,也是將值存儲到該線程的ThreadLocalMap中。這樣,每個線程都擁有自己獨立的變量副本,互不干擾。例如,在數據庫連接管理中,使用ThreadLocal可以為每個線程創建獨立的數據庫連接,避免多線程競爭同一個數據庫連接帶來的問題。示例代碼如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;public class DatabaseConnectionUtil {private static final String URL = "jdbc:mysql://localhost:3306/mydb";private static final String USER = "root";private static final String PASSWORD = "password";private static final ThreadLocal<Connection> threadLocalConnection = ThreadLocal.withInitial(() -> {try {return DriverManager.getConnection(URL, USER, PASSWORD);} catch (SQLException e) {throw new RuntimeException("Failed to create database connection", e);}});public static Connection getConnection() {return threadLocalConnection.get();}public static void closeConnection() {Connection connection = threadLocalConnection.get();if (connection != null) {try {connection.close();} catch (SQLException e) {System.err.println("Error closing database connection: " + e.getMessage());} finally {threadLocalConnection.remove();}}}
}
在上述代碼中,threadLocalConnection是一個ThreadLocal對象,它為每個線程提供獨立的數據庫連接。getConnection()方法用于獲取當前線程的數據庫連接,closeConnection()方法用于關閉當前線程的數據庫連接,并移除ThreadLocal中的連接對象,避免內存泄漏。通過使用ThreadLocal,每個線程都可以獨立地管理自己的數據庫連接,提高了程序的線程安全性和性能 。
>>>博主總結
以上就是 Java 并發編程基礎的核心內容。從線程的基礎概念到啟動終止,再到線程間通信與應用實例,每個知識點都環環相扣。掌握這些內容,能幫助我們編寫出高效、穩定的多線程程序,更能讓我們在面對復雜業務場景時,靈活運用并發技術提升程序性能。并發編程的世界充滿挑戰,但也樂趣無窮,希望大家在實踐中不斷探索,攻克難題,成為 Java 并發編程的高手!