一、多線程介紹
在編程中,我們不可逃避的會遇到多線程的編程問題,因為在大多數的業務系統中需要并發處理,如果是在并發的場景中,多線程就非常重要了。另外,我們在面試的時候,面試官通常也會問到我們關于多線程的問題,如:如何創建一個線程?我們通常會這么回答,主要有兩種方法,第一種:繼承thread類,重寫run方法;第二種:實現runnable接口,重寫run方法。那么面試官一定會問這兩種方法各自的優缺點在哪,不管怎么樣,我們會得出一個結論,那就是使用方式二,因為面向對象提倡少繼承,盡量多用組合。
這個時候,我們還可能想到,如果想得到多線程的返回值怎么辦呢?根據我們多學到的知識,我們會想到實現callable接口,重寫call方法。那么多線程到底在實際項目中怎么使用呢,他有多少種方式呢?
首先,我們來看一個例子:
這是一種創建多線程的簡單方法,很容易理解,在例子中,根據不同的業務場景,我們可以在thread()里邊傳入不同的參數實現不同的業務邏輯,但是,這個方法創建多線程暴漏出來的問題就是反復創建線程,而且創建線程后還得銷毀,如果對并發場景要求低的情況下,這種方式貌似也可以,但是高并發的場景中,這種方式就不行了,因為創建線程銷毀線程是非常耗資源的。所以根據經驗,正確的做法是我們使用線程池技術,jdk提供了多種線程池類型供我們選擇,具體方式可以查閱jdk的文檔。
這里代碼我們需要注意的是,傳入的參數代表我們配置的線程數,是不是越多越好呢?肯定不是。因為我們在配置線程數的時候要充分考慮服務器的性能,線程配置的多,服務器的性能未必就優。通常,機器完成的計算是由線程數決定的,當線程數到達峰值,就無法在進行計算了。如果是耗cpu的業務邏輯(計算較多),線程數和核數一樣就到達峰值了,如果是耗i/o的業務邏輯(操作數據庫,文件上傳、下載等),線程數越多一定意義上有助于提升性能。
線程數大小的設定又一個公式決定:
y=n*((a+b)/a),其中,n:cpu核數,a:線程執行時程序的計算時間,b:線程執行時,程序的阻塞時間。有了這個公式后,線程池的線程數配置就會有約束了,我們可以根據機器的實際情況靈活配置。
二、多線程優化及性能比較
最近的項目中用到了所線程技術,在使用過程中遇到了很多的麻煩,趁著熱度,整理一下幾種多線程框架的性能比較。目前所掌握的大致分三種,第一種:threadpool(線程池)+countdownlatch(程序計數器),第二種:fork/join框架,第三種jdk8并行流,下面對這幾種方式的多線程處理性能做一下比較總結。
首先,假設一種業務場景,在內存中生成多個文件對象,這里暫定30000,(thread.sleep(時間))線程睡眠模擬業務處理業務邏輯,來比較這幾種方式的多線程處理性能。
1) 單線程
這種方式非常簡單,但是程序在處理的過程中非常的耗時,使用的時間會很長,因為每個線程都在等待當前線程執行完才會執行,和多線程沒有多少關系,所以效率非常低。
首先創建文件對象,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
public class fileinfo { private string filename; //文件名 private string filetype; //文件類型 private string filesize; //文件大小 private string filemd5; //md5碼 private string fileversionno; //文件版本號 public fileinfo() { super (); } public fileinfo(string filename, string filetype, string filesize, string filemd5, string fileversionno) { super (); this .filename = filename; this .filetype = filetype; this .filesize = filesize; this .filemd5 = filemd5; this .fileversionno = fileversionno; } public string getfilename() { return filename; } public void setfilename(string filename) { this .filename = filename; } public string getfiletype() { return filetype; } public void setfiletype(string filetype) { this .filetype = filetype; } public string getfilesize() { return filesize; } public void setfilesize(string filesize) { this .filesize = filesize; } public string getfilemd5() { return filemd5; } public void setfilemd5(string filemd5) { this .filemd5 = filemd5; } public string getfileversionno() { return fileversionno; } public void setfileversionno(string fileversionno) { this .fileversionno = fileversionno; } |
接著,模擬業務處理,創建30000個文件對象,線程睡眠1ms,之前設置的1000ms,發現時間很長,整個eclipse卡掉了,所以將時間改為了1ms。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class test { private static list<fileinfo> filelist= new arraylist<fileinfo>(); public static void main(string[] args) throws interruptedexception { createfileinfo(); long starttime=system.currenttimemillis(); for (fileinfo fi:filelist){ thread.sleep( 1 ); } long endtime=system.currenttimemillis(); system.out.println( "單線程耗時:" +(endtime-starttime)+ "ms" ); } private static void createfileinfo(){ for ( int i= 0 ;i< 30000 ;i++){ filelist.add( new fileinfo( "身份證正面照" , "jpg" , "101522" , "md5" +i, "1" )); } } } |
測試結果如下:
可以看到,生成30000個文件對象消耗的時間比較長,接近1分鐘,效率比較低。
2) threadpool (線程池) +countdownlatch (程序計數器)
顧名思義,countdownlatch為線程計數器,他的執行過程如下:首先,在主線程中調用await()方法,主線程阻塞,然后,將程序計數器作為參數傳遞給線程對象,最后,每個線程執行完任務后,調用countdown()方法表示完成任務。countdown()被執行多次后,主線程的await()會失效。實現過程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public class test2 { private static executorservice executor=executors.newfixedthreadpool( 100 ); private static countdownlatch countdownlatch= new countdownlatch( 100 ); private static list<fileinfo> filelist= new arraylist<fileinfo>(); private static list<list<fileinfo>> list= new arraylist<>(); public static void main(string[] args) throws interruptedexception { createfileinfo(); addlist(); long starttime=system.currenttimemillis(); int i= 0 ; for (list<fileinfo> fi:list){ executor.submit( new filerunnable(countdownlatch,fi,i)); i++; } countdownlatch.await(); long endtime=system.currenttimemillis(); executor.shutdown(); system.out.println(i+ "個線程耗時:" +(endtime-starttime)+ "ms" ); } private static void createfileinfo(){ for ( int i= 0 ;i< 30000 ;i++){ filelist.add( new fileinfo( "身份證正面照" , "jpg" , "101522" , "md5" +i, "1" )); } } private static void addlist(){ for ( int i= 0 ;i< 100 ;i++){ list.add(filelist); } } } |
filerunnable類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/** * 多線程處理 * @author wangsj * * @param <t> */ public class filerunnable<t> implements runnable { private countdownlatch countdownlatch; private list<t> list; private int i; public filerunnable(countdownlatch countdownlatch, list<t> list, int i) { super (); this .countdownlatch = countdownlatch; this .list = list; this .i = i; } @override public void run() { for (t t:list){ try { thread.sleep( 1 ); } catch (interruptedexception e) { e.printstacktrace(); } countdownlatch.countdown(); } } } |
測試結果如下:
3) fork/join 框架
jdk從版本7開始,出現了fork/join框架,從字面來理解,fork就是拆分,join就是合并,所以,該框架的思想就是。通過fork拆分任務,然后join來合并拆分后各個人物執行完畢后的結果并匯總。比如,我們要計算連續相加的幾個數,2+4+5+7=?,我們利用fork/join框架來怎么完成呢,思想就是拆分子任務,我們可以把這個運算拆分為兩個子任務,一個計算2+4,另一個計算5+7,這是fork的過程,計算完成后,把這兩個子任務計算的結果匯總,得到總和,這是join的過程。
fork/join框架執行思想:首先,分割任務,使用fork類將大任務分割為若干子任務,這個分割過程需要按照實際情況來定,直到分割出的任務足夠小。然后,join類執行任務,分割的子任務在不同的隊列里,幾個線程分別從隊列里獲取任務并執行,執行完的結果放到一個單獨的隊列里,最后,啟動線程,隊列里拿取結果并合并結果。
使用fork/join框架要用到幾個類,關于類的使用方式可以參考jdk的api,使用該框架,首先需要繼承forkjointask類,通常,只需要繼承他的子類recursivetask或recursiveaction即可,recursivetask,用于有返回結果的場景,recursiveaction用于沒有返回結果的場景。forkjointask的執行需要用到forkjoinpool來執行,該類用于維護分割出的子任務添加到不同的任務隊列。
下面是實現代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
public class test3 { private static list<fileinfo> filelist= new arraylist<fileinfo>(); // private static forkjoinpool forkjoinpool=new forkjoinpool(100); // private static job<fileinfo> job=new job<>(filelist.size()/100, filelist); public static void main(string[] args) { createfileinfo(); long starttime=system.currenttimemillis(); forkjoinpool forkjoinpool= new forkjoinpool( 100 ); //分割任務 job<fileinfo> job= new job<>(filelist.size()/ 100 , filelist); //提交任務返回結果 forkjointask<integer> fjtresult=forkjoinpool.submit(job); //阻塞 while (!job.isdone()){ system.out.println( "任務完成!" ); } long endtime=system.currenttimemillis(); system.out.println( "fork/join框架耗時:" +(endtime-starttime)+ "ms" ); } private static void createfileinfo(){ for ( int i= 0 ;i< 30000 ;i++){ filelist.add( new fileinfo( "身份證正面照" , "jpg" , "101522" , "md5" +i, "1" )); } } } /** * 執行任務類 * @author wangsj * */ public class job<t> extends recursivetask<integer> { private static final long serialversionuid = 1l; private int count; private list<t> joblist; public job( int count, list<t> joblist) { super (); this .count = count; this .joblist = joblist; } /** * 執行任務,類似于實現runnable接口的run方法 */ @override protected integer compute() { //拆分任務 if (joblist.size()<=count){ executejob(); return joblist.size(); } else { //繼續創建任務,直到能夠分解執行 list<recursivetask< long >> fork = new linkedlist<recursivetask< long >>(); //拆分子任務,這里采用二分法 int countjob=joblist.size()/ 2 ; list<t> leftlist=joblist.sublist( 0 , countjob); list<t> rightlist=joblist.sublist(countjob, joblist.size()); //分配任務 job leftjob= new job<>(count,leftlist); job rightjob= new job<>(count,rightlist); //執行任務 leftjob.fork(); rightjob.fork(); return integer.parseint(leftjob.join().tostring()) +integer.parseint(rightjob.join().tostring()); } } /** * 執行任務方法 */ private void executejob() { for (t job:joblist){ try { thread.sleep( 1 ); } catch (interruptedexception e) { e.printstacktrace(); } } } |
測試結果如下:
4) jdk8 并行流
并行流是jdk8的新特性之一,思想就是將一個順序執行的流變為一個并發的流,通過調用parallel()方法來實現。并行流將一個流分成多個數據塊,用不同的線程來處理不同的數據塊的流,最后合并每個塊數據流的處理結果,類似于fork/join框架。
并行流默認使用的是公共線程池forkjoinpool,他的線程數是使用的默認值,根據機器的核數,我們可以適當調整線程數的大小。線程數的調整通過以下方式來實現。
1
|
system.setproperty( "java.util.concurrent.forkjoinpool.common.parallelism" , "100" ); |
以下是代碼的實現過程,非常簡單:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class test4 { private static list<fileinfo> filelist= new arraylist<fileinfo>(); public static void main(string[] args) { // system.setproperty("java.util.concurrent.forkjoinpool.common.parallelism", "100"); createfileinfo(); long starttime=system.currenttimemillis(); filelist.parallelstream().foreach(e ->{ try { thread.sleep( 1 ); } catch (interruptedexception f) { f.printstacktrace(); } }); long endtime=system.currenttimemillis(); system.out.println( "jdk8并行流耗時:" +(endtime-starttime)+ "ms" ); } private static void createfileinfo(){ for ( int i= 0 ;i< 30000 ;i++){ filelist.add( new fileinfo( "身份證正面照" , "jpg" , "101522" , "md5" +i, "1" )); } } } |
下面是測試,第一次沒有設置線程池的數量,采用默認,測試結果如下:
我們看到,結果并不是很理想,耗時較長,接下來設置線程池的數量大小,即添加如下代碼:
1
|
system.setproperty( "java.util.concurrent.forkjoinpool.common.parallelism" , "100" ); |
接著進行測試,結果如下:
這次耗時較小,比較理想。
三、總結
綜上幾種情況來看,以單線程作為參考,耗時最長的還是原生的fork/join框架,這里邊盡管配置了線程池的數量,但效果較精確配置了線程池數量的jdk8并行流較差。并行流實現代碼簡單易懂,不需要我們寫多余的for循環,一個parallelstream方法全部搞定,代碼量大大的減少了,其實,并行流的底層還是使用的fork/join框架,這就要求我們在開發的過程中靈活使用各種技術,分清各種技術的優缺點,從而能夠更好的為我們服務。
原文鏈接:http://www.cnblogs.com/10158wsj/p/8338367.html