多线程的几种实现方式
先上總結:
1.使用實現多線程有四種方式:①繼承Thread類;②實現Runnable接口;③使用Callable和FutureTask實現有返回值的多線程;④使用ExecutorService和Executors工具類實現線程池(如果需要線程的返回值,需要在線程中實現Callable和Future接口)
2.繼承Thread類的優點:簡單,且只需要實現父類的run方法即可(start方法中含有run方法,會創建一個新的線程,而run是執行當前線程)。缺點是:Java的單繼承,如果對象已經繼承了其他的類則不能使用該方法。且不能獲取線程的返回值
3.實現Runnable接口優點:簡單,實現Runnable接口必須實現run方法。缺點:創建一個線程就必須創建一個Runnable的實現類,且不能獲取線程的返CallabTask優點:可以獲取多線程的返回值。缺點:每個多線程都需要創建一個Callable的實現類
4.線程池ExecutorService和工具類Executors優點:可以根據實際情況創建線程數量,且只需要創建一個線程池即可,也能夠通過Callable和Future接口得到線程的返回值,程序的執行時間與線程的數量緊密相關。缺點:需要手動銷毀該線程池(調用shutdown方法)。
盡量不要使用 繼承Thread類 和 實現Runnable接口;盡量使用線程池。否則項目導出都是線程。
在上代碼:
package com.swain.programmingpearls.thread;import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask;/*** thread 的幾種實現*/ public class threadTest {public static void main (String[] args) {//繼承threadExtendsThread extendsThread = new ExtendsThread();extendsThread.start();//實現runnableThread runThread = new Thread(new AchieveRunnable());runThread.start();//調用callable 可以有返回值 可以捕獲異常Callable<String> tc = new TestCallable();FutureTask<String> task = new FutureTask<String>(tc);new Thread(task).start();try {System.out.println(task.get());//獲取返回值} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}//runable 匿名內部類方式new Thread(new Runnable() {@Overridepublic void run() {System.out.println("實現Runnable 匿名內部類方式:" + Thread.currentThread().getName());}}).start();//runnable Lamda表達式new Thread(()->{for (int i = 0; i < 5; i++) {System.out.println("Lamda表達式:" + i);}}).start();System.out.println("主線程");//創建線程池ExecutorService executorService = Executors.newFixedThreadPool(2);for(int i = 0; i<5; i++){AchieveRunnable achieveRunnable = new AchieveRunnable();try {Thread.sleep(1000);} catch (InterruptedException e) {}executorService.execute(achieveRunnable);}//關閉線程池executorService.shutdown();}/*** 繼承thread類*/public static class ExtendsThread extends Thread {public void run(){System.out.println("方法一 繼承Thread線程:" + Thread.currentThread().getName());}}/*** 實現runnable*/public static class AchieveRunnable implements Runnable {@Overridepublic void run() {System.out.println("方法二 實現Runnable:" + Thread.currentThread().getName());}}/*** 通過Callable和FutureTask創建線程*/public static class TestCallable implements Callable<String> {@Overridepublic String call() throws Exception {System.out.println("方法三 實現callable:" + Thread.currentThread().getName());return "我是callable的返回";}} }運行結果: 方法一 繼承Thread線程:Thread-0 方法二 實現Runnable:Thread-1 方法三 實現callable:Thread-2 我是callable的返回 實現Runnable 匿名內部類方式:Thread-3 主線程 Lamda表達式:0 Lamda表達式:1 Lamda表達式:2 Lamda表達式:3 Lamda表達式:4 方法二 實現Runnable:pool-1-thread-1 方法二 實現Runnable:pool-1-thread-2 方法二 實現Runnable:pool-1-thread-1 方法二 實現Runnable:pool-1-thread-2 方法二 實現Runnable:pool-1-thread-1?
關于Concurrent包
concurrent包是在AQS的基礎上搭建起來的,AQS提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架。
線程池參數
我們常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、調度等,使用Executors工廠類創建。
newSingleThreadExecutor可以用于快速創建一個異步線程,非常方便。而newCachedThreadPool永遠不要用在高并發的線上環境,它用的是無界隊列對任務進行緩沖,可能會擠爆你的內存。
我習慣性自定義ThreadPoolExecutor,也就是參數最全的那個。
假如我的任務可以預估,corePoolSize,maximumPoolSize一般都設成一樣大的,然后存活時間設的特別的長。可以避免線程頻繁創建、關閉的開銷。I/O密集型和CPU密集型的應用線程開的大小是不一樣的,一般I/O密集型的應用線程就可以開的多一些。
threadFactory我一般也會定義一個,主要是給線程們起一個名字。這樣,在使用jstack等一些工具的時候,能夠直觀的看到我所創建的線程。
監控
高并發下的線程池,最好能夠監控起來。可以使用日志、存儲等方式保存下來,對后續的問題排查幫助很大。
通常,可以通過繼承ThreadPoolExecutor,覆蓋beforeExecute、afterExecute、terminated方法,達到對線程行為的控制和監控。
阻塞隊列
阻塞隊列會對當前的線程進行阻塞。當隊列中有元素后,被阻塞的線程會自動被喚醒,這極大的提高的編碼的靈活性,非常方便。在并發編程中,一般推薦使用阻塞隊列,這樣實現可以盡量地避免程序出現意外的錯誤。阻塞隊列使用最經典的場景就是socket數據的讀取、解析,讀數據的線程不斷將數據放入隊列,解析線程不斷從隊列取數據進行處理。
ArrayBlockingQueue對訪問者的調用默認是不公平的,我們可以通過設置構造方法參數將其改成公平阻塞隊列。
LinkedBlockingQueue隊列的默認最大長度為Integer.MAX_VALUE,這在用做線程池隊列的時候,會比較危險。
SynchronousQueue是一個不存儲元素的阻塞隊列。每一個put操作必須等待一個take操作,否則不能繼續添加元素。隊列本身不存儲任何元素,吞吐量非常高。對于提交的任務,如果有空閑線程,則使用空閑線程來處理;否則新建一個線程來處理任務”。它更像是一個管道,在一些通訊框架中(比如rpc),通常用來快速處理某個請求,應用較為廣泛。
DelayQueue是一個支持延時獲取元素的無界阻塞隊列。放入DelayQueue的對象需要實現Delayed接口,主要是提供一個延遲的時間,以及用于延遲隊列內部比較排序。這種方式通常能夠比大多數非阻塞的while循環更加節省cpu資源。
另外還有PriorityBlockingQueue和LinkedTransferQueue等,根據字面意思就能猜測它的用途。在線程池的構造參數中,我們使用的隊列,一定要注意其特性和邊界。比如,即使是最簡單的newFixedThreadPool,在某些場景下,也是不安全的,因為它使用了無界隊列。
CountDownLatch
假如有一堆接口A-Y,每個接口的耗時最大是200ms,最小是100ms。
我的一個服務,需要提供一個接口Z,調用A-Y接口對結果進行聚合。接口的調用沒有順序需求,接口Z如何在300ms內返回這些數據?
此類問題典型的還有賽馬問題,只有通過并行計算才能完成問題。歸結起來可以分為兩類:
- 實現任務的并行性
- 開始執行前等待n個線程完成任務
CountDownLatch是通過一個計數器來實現的,計數器的初始值為線程的數量。每當一個線程完成了自己的任務后,計數器的值就會減1。當計數器值到達0時,它表示所有的線程已經完成了任務,然后在閉鎖上等待的線程就可以恢復執行任務。
CyclicBarrier與其類似,可以實現同樣的功能。不過在日常的工作中,使用CountDownLatch會更頻繁一些。
信號量
Semaphore雖然有一些應用場景,但大部分屬于炫技,在編碼中應該盡量少用。
信號量可以實現限流的功能,但它只是常用限流方式的一種。其他兩種是漏桶算法、令牌桶算法
Lock && Condition
在Java中,對于Lock和Condition可以理解為對傳統的synchronized和wait/notify機制的替代。concurrent包中的許多阻塞隊列,就是使用Condition實現的。
End
不管是wait、notify,還是同步關鍵字或者鎖,能不用就不用,因為它們會引發程序的復雜性。最好的方式,是直接使用concurrent包所提供的機制,來規避一些編碼方面的問題。
總結
以上是生活随笔為你收集整理的多线程的几种实现方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows XP系统自动关机
- 下一篇: Simulink Solver