多线程
为什么要用多线程
很显然,多线程能够同时执行多个任务
。举个例子,你打开某视频播放器,点击下载某个视频,然后你发现这个时候一直在下载,其他啥都干不了
。所以在这种情况下,可以使用多线程
,让下载任务继续
,同时也能继续其他操作
。
作为一个包工头
,一堆砖要搬,但是就一个人
,可是你只能搬这么多,怎么办?多找几个人一起搬
,但是其他人就也需要付工钱,没关系,能早点干完也就行了,反正总体工钱差不多。
同样的,如果有一个任务特别耗时
,而这个任务可以拆分为多个任务
,那么就可以让每个线程去执行一个任务
,这样任务就可以更快地完成
了。
代价
听起来都很好,但是多线程是有代价的。由于它们“同时”进行任务
,那么它们任务的有序性就很难保障
,而且一旦任务相关,它们之间可能还会竞争某些公共资源,造成死锁等问题
。
总结
优点:
更快,加快处理任务
更强,同时处理多任务
缺点:
难控制,编程困难
不当使用降低性能,线程切换
bug难定位,资源竞争
排序
多线程排序
思路
假设有N个线程,则将数组数M据分为N组
每个线程对其中的一组数据使用库函数提供的快速排序算法
所有线程排序完成后,将每组排序好的数组合并
第一步
使用4个线程
对11个数据
进行排序:
12,10,4,7,9,6,8,1,5,16,11
由于4不能被10整除
,因此,前面三个线程
,每个负责排序10%(4-1)= 3
三个数,最后一个线程
负责排序最后两个数
。
假设这4个线程都完成了自己的工作后,内容如下:
第二步
比较每组的第一个
,选出最小的一个
,这里是线程2的1
,放到新数组
的下标0处
最后由主线程
将已经排好的每组进行合并
:
将1
放到新数组最开始的位置
,线程的下次比较的内容后移
,即下次比较时,比较线程2的第二个数
。循环比较
最终可以得到合并的数据:
1 4 5 6 7 8 9 10 11 12 16
睡眠排序
睡眠排序
也称为硬件排序
,利用硬件计时器实现
时间复杂度为O(n)
原理
:构造n个线程
,它们和这n个数一一对应
。初始化后
,线程们开始睡眠
,等到对应的那么多个时间单位后各自醒来
,然后输出对应的数
。这样最小的数对应的线程最早醒来
,这个数最早被输出
。等所有线程都醒来,排序就结束
了。
当出现一个很大的数字,会睡眠很长时间(效率低)
当数字相差很小,会不精准(不精准)
不能处理负数(虽然可以在加上一个正数,再进行睡眠排序,但是得不偿失)
排序使用到了多线程,有点“杀鸡焉用宰牛刀”的感觉
多线程
public class SortTest {
public static void main(String[] args) {
int[] an = {123, 22, 9130000, 7, 333, 450, 3};
for (int i = 0; i < an.length; i++) {
new SleepThread(an[i]).start();
}
}
}
class SleepThread extends Thread {
private int number;
SleepThread(int number) {
this.number = number;
}
@Override
public void run() {
try {
Thread.sleep(number);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number + "");
}
}
多线程同步
public class SortTest {
public static void main(String[] args) {
int[] arr = {3, 5, 2, 4, 1, 4, 7, 3, 3000, 0};
System.out.println("原数组" + Arrays.toString(arr));
int[] res = sleepSort(arr);
System.out.println("排序后" + Arrays.toString(res));
}
public static int[] sleepSort(int[] array) {
int[] arr = Arrays.copyOf(array, array.length);
int[] res = new int[arr.length];
AtomicInteger index = new AtomicInteger();
CountDownLatch latch = new CountDownLatch(arr.length);
for (int val : arr) {
new Thread(new SleepSort(val, index, res, latch)).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return res;
}
private static class SleepSort implements Runnable {
private final int val;
private final AtomicInteger index;
private final int[] res;
private final CountDownLatch latch;
SleepSort(int val, AtomicInteger index, int[] res, CountDownLatch latch) {
this.val = val;
this.index = index;
this.res = res;
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(val);
res[index.getAndIncrement()] = val;
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
猴子排序
原理
:一组数字,随机一次
,若随机后的结果不是有序的
,再随机一次
。。。直到随机的结果是有序的
,理论上,只要时间足够长,必定能排好序
。就像一组无序的扑克牌,让猴子打乱一次,若还是无序的,猴子再打乱一次。
太慢啊,O(n*n!)
面条排序
原理
:将输入分别对应到不同长度的面条上
,每根面条的长度即为对应的数字的大小
。比如,对于[1, 4, 2, 8, 9]
这个输入,则分别做出长度为1cm、4cm、2cm、8cm、9cm的面条
。然后,将这些面条的一头对齐
,用手抓住,另一头向下
。然后慢慢地将手向下垂直下降
,第一个触碰到桌面的面条对应的数字则为最大的数字
,第二个触碰到的就是第二大的,依次类推
。
Spaghetti排序简直不是一个软件可行的想法
- 它是一种按物理长度排序
的“物理”理论方法。基本上它说:“将一堆意大利面条棒推到一个平坦的表面上,使最长的那些比最短的更突出 - 从而按长度”排序“它们
。
多线程归并排序
两个线程
分成几个独立的小部分,各个部分让单独的线程去计算。
public static void main(String[] args) {
int length = 10;
int[] arr = (new Data(length)).getData();
System.out.println(Arrays.toString(arr));
int mid = arr.length >> 1;
CountDownLatch latch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {
sort(arr, 0, mid);
latch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sort(arr, mid + 1, arr.length - 1);
latch.countDown();
}
}).start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Arrays.toString(arr));
}
ForkPool/Join 框架
Fork/Join是从JDK 1.7 加入的并发计算框架。
public class MergeSort {
public static void main(String[] args) {
int length = 10;
int[] arr = (new Data(length)).getData();
System.out.println(Arrays.toString(arr));
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new MergeSortFork(arr));
System.out.println(Arrays.toString(arr));
}
public static class MergeSortFork extends RecursiveAction {
private static final long serialVersionUID = 425572392953885545L;
private int[] arr;
public MergeSortFork(int[] arr) {
this.arr = arr;
}
@Override
protected void compute() {
sort(arr,0,arr.length-1);
}
}
//下面就是普通归并排序
public static int[] sort(int[] arr, int l, int r) {
int mid = ((r - l) >> 1) + l;
if (l < r) {
sort(arr, l, mid);
sort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
return arr;
}
public static void merge(int[] arr, int l, int mid, int r) {
int[] temp = new int[r - l + 1];
int i = l;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= r) {
temp[k++] = arr[i] > arr[j] ? arr[j++] : arr[i++];
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= r) {
temp[k++] = arr[j++];
}
for (int x = 0; x < temp.length; x++) {
arr[l + x] = temp[x];
}
}
}
class Data {
int length;
int[] arr;
public Data(int length) {
this.length = length;
arr = new int[length];
}
public int[] getData() {
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < length; i++) {
arr[i] = random.nextInt(2 * length);
}
return arr;
}
}
通过统计可以发现,当待排序序列长度较小时
,使用单线程效率要高于多线程
,但是随着数量不断增加
,多线程执行时间越来越接近单线程的执行时间
,最终在1000万
这个量级开始速率远超单线程。工作中不能滥用多线程,在该使用的时候使用可以加快效率,充分利用多核
。但是在不该用的时候使用徒增工作量,有可能效率还不如单线程。
100w数据处理的思想
在大量的数据中
,寻找最大的k个数
,或者是出现次数最多的k个数据
,比如说这个数据有10个G
,放在一个大文件中,电脑内存4G
。
解题思路就是先把这个文件分块
,为了确保相同的数据在一个块中
,通过计算Hash值来分块
,相同Hash 放到一个块中
。
比如每分100个
块,这样平均一个块就在100M左右
,对每个块分别载入内存
找最大的前K个数
或者出现最多的前K个数据
,最后比较这100*K个数据
来得到结果。