如果您的應用程序與那些能夠同時處理多個任務的應用程序相比表現不佳,很可能是因為它是單線程的。解決這個問題的方法之一是采用多線程技術。
以下是一些可以考慮的方法:
- 線程(Thread)
- 并行流(Parallel Streams)
- ExecutorService
- ForkJoinPool
- CompletableFuture
適當地使用這些方法,可以徹底改變您的應用程序,并推動您的職業發展。下面我們來看看如何將您的應用程序轉變為高效的多線程應用。
1. 線程(Thread)
第一種選擇是使用線程(Thread)類。通過這種方式,您可以直接控制線程的創建和管理。以下是一個示例:
CustomTask 每隔50毫秒從0數到 count - 1。
public class CustomTask implements Runnable {
private final String name;
private final int count;
CustomTask(String name, int count) {
this.name = name;
this.count = count;
}
@Override
public void run() {
for (int i = 0; i < count; i++) {
System.out.println(name + "-" + i + " from " +
Thread.currentThread().getName());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
a、b 和 c 是該類的三個實例。
Thread a = new Thread(new CustomTask("a", 5));
Thread b = new Thread(new CustomTask("b", 10));
Thread c = new Thread(new CustomTask("c", 5));
請注意,b 預計計數的次數是其他實例的兩倍。您希望在 a 和 c 順序運行的同時運行 b。
您可以非常容易地實現這種行為。
// 首先啟動 a 和 b。
a.start();
b.start();
// a 完成后開始 c。
a.join();
c.start();
以下是結果:
a-0 from Thread-0
b-0 from Thread-1
b-1 from Thread-1
a-1 from Thread-0
b-2 from Thread-1
a-2 from Thread-0
b-3 from Thread-1
a-3 from Thread-0
b-4 from Thread-1
a-4 from Thread-0
b-5 from Thread-1
c-0 from Thread-2
b-6 from Thread-1
c-1 from Thread-2
b-7 from Thread-1
c-2 from Thread-2
b-8 from Thread-1
c-3 from Thread-2
b-9 from Thread-1
c-4 from Thread-2
a 和 b 同時開始運行,輪流輸出。a 完成后,c 開始執行。此外,它們全部在不同的線程中運行。通過手動創建 Thread 實例,您可以完全控制它們。
然而,請注意,低級線程處理也需要同步和資源管理,這可能更容易出錯和復雜。
2. 并行流(Parallel Streams)
當您需要對大型集合中的所有元素應用相同、重復且獨立的任務時,并行流非常有效。
例如,圖像調整大小是一個需要按順序運行的繁重任務;當您有多個圖像需要調整大小時,如果按順序執行,將需要很長時間才能完成。在這種情況下,您可以使用并行流并行調整它們的大小,如下所示。
private static List<BufferedImage> resizeAll(List<BufferedImage> sourceImages,
int width, int height) {
return sourceImages
.parallelStream()
.map(source -> resize(source, width, height))
.toList();
}
這樣,圖像將同時調整大小,節省了大量寶貴的時間。
3. ExecutorService
當實現不需要精確的線程控制時,可以考慮使用 ExecutorService。ExecutorService 提供了更高層次的線程管理抽象,包括線程池、任務調度和資源管理。
ExecutorService 是一個接口,它最常見的用法是線程池。假設您有大量的異步任務堆積在一起,但是同時運行所有任務——每個任務占用一個線程——似乎太多了。線程池可以通過限制最大線程數來幫助您。
下面的示例中,我們使用 Executors.newFixedThreadPool() 實例化 ExecutorService 來使用 3 個線程運行 10 個任務。每個任務只打印一行。請注意,我們在之前的部分中重用了之前定義的 CustomTask。
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(new CustomTask(String.valueOf(i), 1));
}
executorService.shutdown();
這將打印以下結果:
0-0 from pool-1-thread-1
2-0 from pool-1-thread-3
1-0 from pool-1-thread-2
4-0 from pool-1-thread-3
3-0 from pool-1-thread-2
5-0 from pool-1-thread-1
6-0 from pool-1-thread-1
7-0 from pool-1-thread-3
8-0 from pool-1-thread-2
9-0 from pool-1-thread-3
10 個任務在 3 個線程中運行。通過限制特定任務使用的線程數,您可以根據優先級分配線程數:對于重要且頻繁的任務使用更多線程,對于瑣碎或偶爾的任務使用較少線程。ExecutorService 具有高效和簡潔的特點,是大多數多線程場景的首選選項。
如果您需要更多的控制和靈活性,請查看 ThreadPoolExecutor,它是 Executors.newFixedThreadPool() 返回的 ExecutorService 的實際實現。您可以直接創建其實例或將返回的 ExecutorService 實例轉換為 ThreadPoolExecutor 實例以獲得更多控制權。
4. ForkJoinPool
ForkJoinPool是另一種線程池,正如其名稱所示。雖然它在許多其他異步方法的底層使用中,但對于可以分解為較小且獨立子任務的任務來說,它也非常強大,這些任務可以通過分而治之的策略來解決。
其中一個任務是圖像調整大小。圖像調整大小是分而治之問題的一個很好的例子。使用ForkJoinPool,您可以將圖像分成兩個或四個較小的圖像,并同時調整它們的大小。以下是ImageResizeAction的示例,它將圖像調整為給定的大小。
package multithreading;
import JAVA.awt.image.BufferedImage;
import java.util.concurrent.RecursiveAction;
public class ImageResizeAction extends RecursiveAction {
private static final int THRESHOLD = 100;
private final BufferedImage sourceImage;
private final BufferedImage targetImage;
private final int startRow;
private final int endRow;
private final int targetWidth;
private final int targetHeight;
public ImageResizeAction(BufferedImage sourceImage,
BufferedImage targetImage,
int startRow, int endRow,
int targetWidth, int targetHeight) {
this.sourceImage = sourceImage;
this.targetImage = targetImage;
this.startRow = startRow;
this.endRow = endRow;
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
}
@Override
protected void compute() {
if (endRow - startRow <= THRESHOLD) {
resizeImage();
} else {
int midRow = startRow + (endRow - startRow) / 2;
invokeAll(
new ImageResizeAction(sourceImage, targetImage,
startRow, midRow, targetWidth, targetHeight),
new ImageResizeAction(sourceImage, targetImage,
midRow, endRow, targetWidth, targetHeight)
);
}
}
private void resizeImage() {
int sourceWidth = sourceImage.getWidth();
double xScale = (double) targetWidth / sourceWidth;
double yScale = (double) targetHeight / sourceImage.getHeight();
for (int y = startRow; y < endRow; y++) {
for (int x = 0; x < sourceWidth; x++) {
int targetX = (int) (x * xScale);
int targetY = (int) (y * yScale);
int rgb = sourceImage.getRGB(x, y);
targetImage.setRGB(targetX, targetY, rgb);
}
}
}
}
請注意,ImageResizeAction繼承了RecursiveAction。RecursiveAction用于定義遞歸的調整大小操作。在此示例中,圖像被分成兩半并并行調整大小。
您可以使用以下代碼運行ImageResizeAction:
public static void mAIn(String[] args) throws IOException {
String sourceImagePath = "source_image.jpg";
String targetImagePath = "target_image.png";
int targetWidth = 300;
int targetHeight = 100;
BufferedImage sourceImage = ImageIO.read(new File(sourceImagePath));
BufferedImage targetImage = new BufferedImage(targetWidth, targetHeight,
BufferedImage.TYPE_INT_RGB);
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.invoke(new ImageResizeAction(sourceImage, targetImage,
0, sourceImage.getHeight(), targetWidth, targetHeight));
ImageIO.write(targetImage, "png", new File(targetImagePath));
System.out.println("圖像調整大小成功!");
}
借助ForkJoinPool的幫助,您現在能夠更高效地調整圖像的大小,具有更好的可伸縮性,并最大程度地利用資源。
5. CompletableFuture
通過CompletableFuture,您可以完全發揮Future的功能,并擁有許多額外的特性。其中最突出的功能是它能夠鏈式地連接異步操作,使您能夠構建復雜的異步管道。
public static void main(String[] args) {
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
return "Hyuni Kim";
}).thenApply((data) -> {
System.out.println(Thread.currentThread().getName());
return "我的名字是" + data;
}).thenAccept((data) -> {
System.out.println(Thread.currentThread().getName());
System.out.println("結果:" + data);
});
future.join();
}
上述代碼展示了CompletableFuture的一個關鍵方面:鏈式操作。通過CompletableFuture.supplyAsync(),首先創建并運行一個返回字符串結果的CompletableFuture。thenApply()接受前一個任務的結果,并執行其他操作,本例中是添加一個字符串。最后,thenAccept()打印生成的數據。結果如下所示:
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-1
Result: My name is Hyuni Kim
有3個任務沒有在主線程中運行,這表明它們與主邏輯并行運行。當您有具有結果并需要鏈接的任務時,CompletableFuture將是一個很好的選擇。
6. 總結
多線程是一種強大的工具,可以幫助開發人員優化性能、提升用戶體驗、增強并發處理能力,并充分利用計算機的資源。