執行緒的建立和生命週期涉及到執行緒的產生、執行和結束過程。讓我們繼續深入探索這個主題:
執行緒的建立方式有多種,你可以選擇適合你場景的方式:
繼承Thread類: 建立一個類,繼承自Thread類,並重寫run()方法。通過範例化這個類的物件,並呼叫start()方法,系統會自動呼叫run()方法執行執行緒邏輯。
public class MyThread extends Thread {
public void run() {
// 執行緒邏輯程式碼
}
}
// 建立並啟動執行緒
MyThread thread = new MyThread();
thread.start();
實現Runnable介面: 建立一個類,實現Runnable介面,並實現run()方法。通過將實現了Runnable介面的物件作為引數傳遞給Thread類別建構函式,然後呼叫start()方法啟動執行緒。
public class MyRunnable implements Runnable {
public void run() {
// 執行緒邏輯程式碼
}
}
// 建立並啟動執行緒
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
實現Callable介面: 建立一個類,實現Callable介面,並實現call()方法。通過建立一個FutureTask物件,將Callable物件作為引數傳遞給FutureTask建構函式,然後將FutureTask物件傳遞給Thread類別建構函式,最後呼叫start()方法啟動執行緒。
public class MyCallable implements Callable<Integer> {
public Integer call() {
// 執行緒邏輯程式碼
return 1;
}
}
// 建立並啟動執行緒
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
通過執行緒池建立執行緒: 使用Java的執行緒池ExecutorService來管理執行緒的生命週期。通過提交Runnable或Callable任務給執行緒池,執行緒池會負責建立、執行和終止執行緒。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new Runnable() {
public void run() {
// 執行緒邏輯程式碼
}
});
executorService.shutdown();
執行緒的生命週期經歷以下幾個狀態:
理解執行緒的建立和生命週期對於處理並行程式設計非常重要。通過選擇合適的建立方式和正確地管理執行緒的生命週期,可以確保執行緒安全、高效地執行,從而優化程式效能。
synchronized關鍵字在Java中用於實現執行緒安全的程式碼塊,在其背後使用JVM底層內建的鎖機制。synchronized的設計考慮了各種並行情況,因此具有以下優點:
synchronized的鎖機制包括以下幾個階段的升級過程:
需要注意的是,如果在輕量級鎖狀態下,有執行緒獲取物件的HashCode時,會直接升級為重量級鎖。這是因為鎖升級過程中使用的mark頭將HashCode部分隱去,以確保鎖升級過程的正確性。
底層實現中,synchronized使用了monitor enter和monitor exit指令來進行進入鎖和退出鎖的同步操作。對於使用者來說,這些操作是不可見的。synchronized鎖的等待佇列儲存在物件的waitset屬性中,用於執行緒的等待和喚醒操作。
範例程式碼:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有構造方法
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
為什麼需要使用volatile: 雖然synchronized關鍵字可以確保執行緒安全,但是如果沒有volatile修飾,可能會發生指令重排的問題。volatile關鍵字的主要作用是防止指令重排,保證可見性和有序性。盡
管在實際工作中很少遇到指令重排導致的問題,但是理論上存在這種可能性,因此使用volatile修飾變數可以避免出現意外情況。
指令重排原因及影響: 指令重排是為了優化程式的執行速度,由於CPU的工作速度遠大於記憶體的工作速度,為了充分利用CPU資源,處理器會對指令進行重新排序。例如在建立一個物件的過程中,通常被拆分為三個步驟:1)申請空間並初始化,2)賦值,3)建立地址連結關係。如果沒有考慮逃逸分析,可能會發生指令重排的情況。
這種重排可能導致的問題是,當一個執行緒在某個時刻執行到步驟2,而另一個執行緒在此時獲取到了物件的參照,但是這個物件還沒有完成初始化,導致使用到未完全初始化的物件,可能會出現異常或不正確的結果。通過使用volatile關鍵字,可以禁止指令重排,確保物件的完全初始化後再進行賦值操作。
抽象佇列同步器(Abstract Queued Synchronizer)是Java並行程式設計中非常重要的同步框架,被廣泛應用於各種鎖實現類,如ReentrantLock、CountDownLatch等。AQS提供了基於雙端佇列的同步機制,支援獨佔模式和共用模式,並提供了一些基本的操作方法。
在AQS中,用來表示是否是獨佔鎖的Exclusive屬性物件非常重要。它可以控制同一時間只有一個執行緒能夠獲取鎖,並且支援重入機制。另外,AQS的state屬性也非常關鍵,state的含義和具體用途是由具體的子類決定的。子類可以通過對state屬性的操作來實現不同的同步邏輯。例如,在ReentrantLock中,state表示鎖的持有數;在CountDownLatch中,state表示還需要等待的執行緒數。
此外,AQS還使用兩個Node節點來表示雙端佇列,用於儲存被阻塞的執行緒。這些節點會根據執行緒的不同狀態(如等待獲取鎖、等待釋放鎖)被新增到佇列的不同位置,從而實現執行緒同步和排程。
以下是一個簡化的範例程式碼,展示瞭如何使用ReentrantLock和AQS進行執行緒同步:
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
// 執行執行緒1的邏輯
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
// 執行執行緒2的邏輯
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
}
}
在上述程式碼中,我們使用了ReentrantLock作為鎖工具,它內部使用了AQS來實現執行緒同步。通過呼叫lock()方法獲取鎖,並在finally塊中呼叫unlock()方法釋放鎖,確保執行緒安全執行。這樣,只有一個執行緒能夠獲取到鎖,並執行相應的邏輯。
總之,AQS作為Java執行緒同步的核心框架,在並行程式設計中起到了至關重要的作用。它提供了強大的同步機制,可以支援各種鎖的實現,幫助我們實現執行緒安全的程式碼。
使用Java執行緒池是一種優化並行性的有效方式。執行緒池可以管理和複用執行緒,減少了執行緒建立和銷燬的開銷,提高了系統的效能和資源利用率。
在Java中,可以使用ExecutorService介面來建立和管理執行緒池。ExecutorService提供了一些方法來提交任務並返回Future物件,可以用於獲取任務的執行結果。
在建立執行緒池時,可以根據實際需求選擇不同的執行緒池型別。常用的執行緒池型別包括:
使用執行緒池時,可以將任務分解為多個小任務,提交給執行緒池並行執行。這樣可以充分利用系統資源,提高任務執行的並行性。
同時,執行緒池還可以控制並行執行緒的數量,避免系統資源耗盡和任務過載的問題。通過設定合適的執行緒池大小,可以平衡系統的並行能力和資源消耗。
Fork/Join框架是Java中用於處理並行任務的一個強大工具。它基於分治的思想,將大任務劃分成小任務,並利用多執行緒並行執行這些小任務,最後將結果合併。
在Fork/Join框架中,主要有兩個核心類:ForkJoinTask和ForkJoinPool。ForkJoinTask是一個可以被分割成更小任務的任務,我們需要繼承ForkJoinTask類並實現compute()方法來定義具體的任務邏輯。ForkJoinPool是一個執行緒池,用於管理和排程ForkJoinTask。
下面是一個簡單的例子,展示如何使用Fork/Join框架來計算一個整數陣列的總和:
import java.util.concurrent.*;
public class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 10;
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork(); // 將左半部分任務提交到執行緒池
rightTask.fork(); // 將右半部分任務提交到執行緒池
int leftResult = leftTask.join(); // 等待左半部分任務的完成並獲取結果
int rightResult = rightTask.join(); // 等待右半部分任務的完成並獲取結果
return leftResult + rightResult;
}
}
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
ForkJoinPool forkJoinPool = new ForkJoinPool();
SumTask sumTask = new SumTask(array, 0, array.length);
int result = forkJoinPool.invoke(sumTask); // 使用執行緒池來執行任務
System.out.println("Sum: " + result);
}
}
在這個例子中,我們定義了一個SumTask類,繼承自RecursiveTask類,並實現了compute()方法。在compute()方法中,我們判斷任務的大小是否小於閾值,如果是,則直接計算陣列的總和;如果不是,則將任務劃分成兩個子任務,並使用fork()方法將子任務提交到執行緒池中,然後使用join()方法等待子任務的完成並獲取結果,最後返回子任務結果的和。
在main()方法中,我們建立了一個ForkJoinPool物件,然後建立了一個SumTask物件,並使用invoke()方法來執行任務。最後列印出結果。
通過使用Fork/Join框架,我們可以方便地處理並行任務,並利用多核處理器的效能優勢。這個框架在處理一些需要遞迴分解的問題時非常高效。
文章涉及了幾個常見的並行程式設計相關的主題。首先,執行緒的建立和生命週期是面試中常被問及的話題,面試官可能會詢問如何建立執行緒、執行緒的狀態轉換以及如何控制執行緒的執行順序等。其次,synchronized關鍵字是用於實現執行緒同步的重要工具,面試中可能會涉及到它的使用場景以及與其他同步機制的比較。此外,抽象佇列同步器(AQS)是Java並行程式設計中的核心概念,瞭解其原理和應用場景可以展示對並行程式設計的深入理解。最後,面試中可能會考察對Java執行緒池和Fork/Join框架的瞭解,包括它們的使用方法、優勢和適用場景等。種子題目務必學會