前言:多線程搜索數組和排序在實際開發中是一個很常見的場景,我們可能會通過數組保存一些業務數據,通過搜索達到自己想要的數據或者對數據按照一定的業務規則排序,而在技術選擇上一般最常見的技術就是for循環遍歷和各種排序算法,這種搜索/排序技術很簡單,而我們今天將要探討的是通過多線程搜索和排序,如何利用多線程的優勢去高效的完成搜索和排序是本篇博客聚焦的重點
本篇博客目錄
一:多線程搜索
二:所線程排序
三:總結
一:多線程搜索
1.1:創建線程池
為了提升多線程的性能,我們把線程放在線程池集中統一管理,這樣可以最大限度的減少線程切換帶來的靠小。線程池的種類有5種,這里我們選擇使用cached緩存池,先定義線程數量為2,也就是每次展開兩個線程去進行搜索。為了防止一個線程已經找到值,而其它線程繼續工作的方法,我們可以用AtomicInteger 的無鎖cas來保存返回的結果,避免一個線程搜索到值,而其它線程還在繼續搜索的問題。在定義一個數組,注意:這里的數組元素只有13個,數組數量較少,多線程性能搜索提升不是很明顯。
public class ManyThreadSearch { public static ExecutorService pool = Executors.newCachedThreadPool();//創建一個緩存線程池 public static final int Thread_NUM=2;//定義線程數量為2 public static AtomicInteger result= new AtomicInteger(-1);//最終返回的結果值 默認為-1 private static int[] array={2,8,5,3,8,9,3,4,26,76,46,8};//搜索的數組
1.2:搜索方法
這里也是采用for循環的方式,不用的是我們定義了begin和end點,那么線程將會在其中這個區間進行搜索。而如果其中一個線程找到的值的話,會通過cas技術將result的值設為找到的元素的下標值,而expect預期值是-1,也就是result的默認值。不理解的同學請參考上上篇博客,了解一下cas技術。整理邏輯比較簡單,就是for循環查找,注意:這個方法是每個線程都會調用的方法。
/** * 搜索方法 * @param searchValue 搜索的值 * @param beign 開始 * @param end 結束 * @return 搜索元素的下標 */ public static int search(int searchValue,int beign,int end){ int i=0; for (i=beign;i<end;i++){ if (result.get()>0){ return result.get(); } if (array[i]==searchValue){ if (result.compareAndSet(-1,i)){ //利用cas防止線程重復搜索 return result.get(); } return i;//返回元素下標 } } return -1; }
1.3:搜索線程
搜索線程就是進行搜索的單個線程,這里讓該類繼承自Callable而不是Thread,主要的原因是我們要借用Future模式,並且因為搜索是要有返回值的,而Thread的run方法不可以有返回值。然后將一些固定值通過構造方法傳送給這個線程類,在call方法里調用search方法得到返回值。
/** * 搜索線程 這里不用Thread ,因為Thread不可以有返回值 */ public static class SearchThread implements Callable<Integer> { private int begin;int end;int searchValue; public SearchThread(int searchValue,int begin, int end){ this.begin = begin; this.end = end; this.searchValue = searchValue; } @Override public Integer call() throws Exception { //Call方法就好比Thread中的run方法 int re=search(searchValue,begin,end); return re; } }
1.4:最終的搜索方法
此方法只需要傳入一個搜索值即可,這里使用了數組的切割的方式,按照線程的數量和數組的大小自動分配子數組,每個線程去搜索固定的子數組,然后將結果返回,存在Future中,Future表示最終的搜索結果,其中只要有一個線程返回結果,其他線程立刻停止搜索。
/** * 用線程搜索的方法 * @param searchValue 搜索的值 * @return * @throws ExecutionException 執行的異常 * @throws InterruptedException 被打斷的異常 */ public static int eSerach(int searchValue) throws ExecutionException, InterruptedException { //subArrSize=3; int subArrSize = array.length / Thread_NUM + 1; ArrayList<Future<Integer>> result = new ArrayList<>(); for (int i = 0; i < array.length; i+=subArrSize) { int end=i+subArrSize; if (end>array.length){ end=array.length; } Future<Integer> future = pool.submit(new SearchThread( searchValue,i, end));//線程池開始工作,提交線程,保存返回所有的結果值 result.add(future); } for (Future<Integer> fu:result){ if (fu.get()>=0){ return fu.get(); } } return -1; }
二:多線程排序
2.1:前言
排序在我們日常的編程中也是非常常見的,比如mysql的排序,按照時間、id的大小排序,當然mysql內部使用的B+樹排序。關於排序算法有很多,比如冒泡排序、快速排序、堆排序等等,這里不作深入介紹,我們來做一個簡單的面試題:給一個數組,將奇數和偶數分離,這個用快速排序的思路很容易時間,當然了本篇博客的主題是多線程,那么我們就來探究一下使用多線程來進行數組的排序
2.2:冒泡排序回顧
冒泡排序可以說是最簡單的排序算法了,類似於自然界的小氣泡在最下面,大氣泡在前面的現象,將數組從小到大比較交換排序
/** * 冒泡排序 * @param arr 數組 */ public static void bubbleSort(int[] arr){ for (int i = arr.length-1; i >0;--) { for (int j=0;j<i;j++){ //交換數組順序 if (arr[j]>arr[j+1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } }
2.3:奇偶索引分離排序算法
奇數和偶數索引分離排序算法的思路是:將數組按照索引分成奇數和偶數索引,然后將線程划分,一些只按照奇數索引排序,一些只按照偶數索引排序,最后再將結果合並在一起,這樣就可以做到相互不影響,實現排序。可以看見下面的代碼首先創建一個緩存的線程池,然后定義了一個數組,flag標記用來表示是否發生了數據交換,而在設置flag的時候加上了synchronzied鎖控制,其目的就是放了方式多線程的並發混亂問題。
/** * Created by Yiron on 7/10 0010. * * @desc 多線程排序 */ public class ManyThreadSort { public static ExecutorService pool = Executors.newCachedThreadPool();//創建一個緩存線程池; public static int[] array = {1, 7, 56, 45, 45, 34, 343, 4, 9, 35, 45, 45}; public static int flag = 1;//是否發生數據交換的標記 public static synchronized void setFlag(int expect) {//加鎖控制 防止標志被其他線程改寫 flag = expect; } public static synchronized int getFlag() { return flag; }
2.4:數據交換線程
定義一個專門用作奇數和偶數索引排序的線程,其中用到了兩個變量,一個是數組的索引,一個是CountDownLatch,這個類的作用是控制線程等待,用於同步控制線程,類似於Join,其中每一交換一次數據,都會調用它的countDown方法,將計數器-1,直到計數器為0的時候就會釋放所有的等待線程。關於對CountDownLatch的理解如下:
CountDownLatch能夠使一個線程在等待另外一些線程完成各自工作之后,再繼續執行。使用一個計數器進行實現。計數器初始值為線程的數量。當每一個線程完成自己任務后,計數器的值就會減一。當計數器的值為0時,表示所有的線程都已經完成了任務,然后在CountDownLatch上等待的線程就可以恢復執行任務。
這個工具類的作用其實和synchyonzied的作用差不多,但是它要比鎖機制要更靈活和高效,當每次線程運行的時候(在線程池里),計數器就會減1(countDown方法),而在這期間其它線程是等待狀態(await方法作用),當為0的時候,main線程就會繼續運行,
public static class OddEvenSortThread implements Runnable { int i; CountDownLatch latch; public OddEvenSortThread(int i, CountDownLatch latch) { this.i = i; this.latch = latch; } @Override public void run() { if (array[i] > array[i + 1]) { //如果前面的數組元素大於后面的元素,交換順序 int temp = array[i]; array[i] = array[i + 1]; array[i + 1] = temp; setFlag(1); } latch.countDown(); } }
2.5:排序算法
在這個排序算法中,其中start的作用是表示進行的是奇數交換還是偶數交換,0表示的是偶數交換,1表示奇數交換,程序開始,首先就是發生數據交換或者奇數交換情況下,然后首先置flag為0,表示未發生任何數據交換,創建一個計數器大小為數組長度的二分之一減去一個固定值,這里判斷了固定值的大小,如果數組長度是偶數,則減去1,否則減去0;然后就行for循環去運行線程排序了。
/** * 排序算法 * * @param array 數組 * @throws InterruptedException */ public static void eSort(int[] array) throws InterruptedException { int start = 0; while (getFlag() == 1 || start == 1) { setFlag(0); CountDownLatch countDownLatch = new CountDownLatch(array.length / 2 - (array.length % 2 == 0 ? start : 0)); for (int i = start; i < array.length - 1; i += 2) { pool.submit(new OddEvenSortThread(i, countDownLatch)); } countDownLatch.await(); if (start == 0) { //奇數和偶數切換,防止一直進行偶排序或者奇數排序 start = 1; } else { start = 0; } } }
運行程序可以發現線程將數組排序好了:
1 4 7 9 34 35 45 45 45 45 56 343
Process finished with exit code
三:總結
本篇博客講了數組多線程的搜索與排序技術,其中線程的調度使用了線程池的CachedThreadPool,而在排序中使用了CoutDownLatch這個線程工具類,該類有點類似於join,但是比join強大。值得一提的是如果在數組量比較小的情況下,多線程帶來的性能提升並不是很大,甚至小於單線程的程序,但是一旦在數據量比特別大的時候,多線程的作用就顯而易見的發揮出來了。當然本篇博客只是淺嘗輒止,只是多線程的技術的一些拋磚引玉。我后序會繼續在多線程這里探究,謝謝。