前言:多线程搜索数组和排序在实际开发中是一个很常见的场景,我们可能会通过数组保存一些业务数据,通过搜索达到自己想要的数据或者对数据按照一定的业务规则排序,而在技术选择上一般最常见的技术就是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强大。值得一提的是如果在数组量比较小的情况下,多线程带来的性能提升并不是很大,甚至小于单线程的程序,但是一旦在数据量比特别大的时候,多线程的作用就显而易见的发挥出来了。当然本篇博客只是浅尝辄止,只是多线程的技术的一些抛砖引玉。我后序会继续在多线程这里探究,谢谢。