场景题算法
1: 实战算法篇
1、URL黑名单(布隆过滤器)
100亿黑名单URL,每个64B,问这个黑名单要怎么存?判断一个URL是否在黑名单中
散列表:
如果把黑名单看成一个集合,将其存在 hashmap 中,貌似太大了,需要 640G,明显不科学。
布隆过滤器:
它实际上是一个很长的二进制矢量和一系列随机映射函数。
它可以用来判断一个元素是否在一个集合中。它的优势是只需要占用很小的内存空间以及有着高效的查询效率。对于布隆过滤器而言,它的本质是一个位数组:位数组就是数组的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。
在数组中的每一位都是二进制位。布隆过滤器除了一个位数组,还有 K 个哈希函数。当一个元素加入布隆过滤器中的时候,会进行如下操作:
- 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
2、词频统计(分文件)
2GB内存在20亿整数中找到出现次数最多的数
通常做法是使用哈希表对出现的每一个数做词频统计,哈希表的key是某个整数,value记录整数出现的次数。本题的数据量是20亿,有可能一个数出现20亿次,则为了避免溢出,哈希表的key是32位(4B),value也是 32位(4B),那么一条哈希表的记录就需要占用8B。
当哈希表记录数为2亿个时,需要16亿个字节数(8*2亿),需要至少1.6GB内存(16亿/2^30,1GB== 2 ^30个字节 == 10亿)。则20亿个记录,至少需要16GB的内存,不符合题目要求。
解决办法是将20亿个数的大文件利用哈希函数分成16个小文件,根据哈希函数可以把20亿条数据均匀分布到16个文件上,同一种数不可能被哈希函数分到不同的小文件上,假设哈希函数够好。然后对每一个小文件用哈希函数来统计其中每种数出现的次数,这样我们就得到16个文件中出现次数最多的数,接着从16个数中选出次数最大的那个key即可。
3、未出现的数(bit数组)
40亿个非负整数中找到没有出现的数
对于原问题,如果使用哈希表来保存出现过的数,那么最坏情况下是40亿个数都不相同,那么哈希表则需要保存40亿条数据,一个32位整数需要4B,那么40亿*4B = 160亿个字节,一般大概10亿个字节的数据需要1G的空间,那么大概需要16G的空间,这不符合要求。
我们换一种方式,申请一个bit数组,数组大小为4294967295,大概为40亿bit,40亿/8 = 5亿字节,那么需要0.5G空间, bit数组的每个位置有两种状态0和1,那么怎么使用这个bit数组呢?呵呵,数组的长度刚好满足我们整数的个数范围,那么数组的每个下标值对应4294967295中的一个数,逐个遍历40亿个无符号数,例如,遇到20,则bitArray[20] = 1;遇到666,则bitArray[666] = 1,遍历完所有的数,将数组相应位置变为1。
40亿个非负整数中找到一个没有出现的数,内存限制10MB
10亿个字节的数据大概需要1GB空间处理,那么10MB内存换算过来就是可以处理1千万字节的数据,也就是8千万bit,对于40亿非负整数如果申请bit数组的话,40亿bit / 0.8亿bit = 50,那么这样最少也得分50块来处理,下面就以64块来进行分析解答吧。
总结一下进阶的解法:
1.根据10MB的内存限制,确定统计区间的大小,就是第二次遍历时的bitArr大小。
2.利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。
3.对这个区间上的数做bit map映射,再遍历bit map,找到一个没出现的数即可。
自己的想法
如果只是找一个数,可以高位模运算,写到64个不同的文件,然后在最小的文件中通过bitArray一次处理掉。
40亿个无符号整数,1GB内存,找到所有出现两次的数
对于原问题,可以用bit map的方式来表示数出现的情况。具体地说,是申请一个长度为4294967295×2的bit类型的数组bitArr,用2个位置表示一个数出现的词频,1B占用8个bit,所以长度为4294967295×2的bit类型的数组占用1GB空间。怎么使用这个bitArr数组呢?遍历这40亿个无符号数,如果初次遇到num,就把bitArr[num2 + 1]和bitArr[num2]设置为01,如果第二次遇到num,就把bitArr[num2+1]和bitArr[num2]设置为10,如果第三次遇到num,就把bitArr[num2+1]和bitArr[num2]设置为11。以后再遇到num,发现此时bitArr[num2+1]和bitArr[num2]已经被设置为11,就不再做任何设置。遍历完成后,再依次遍历bitArr,如果发现bitArr[i2+1]和bitArr[i2]设置为10,那么i 就是出现了两次的数。
4、重复URL(分机器)
找到100亿个URL中重复的URL
原问题的解法使用解决大数据问题的一种常规方法:把大文件通过哈希函数分配到机器,或者通过哈希函数把大文件拆成小文件。一直进行这种划分,直到划分的结果满足资源限制的要求。首先,你要向面试官询问在资源上的限制有哪些,包括内存、计算时间等要求。在明确了限制要求之后,可以将每条URL通过哈希函数分配到若干机器或者拆分成若干小文件,这里的“若干”由具体的资源限制来计算出精确的数量。
例如,将100亿字节的大文件通过哈希函数分配到100台机器上,然后每一台机器分别统计分给自己的URL中是否有重复的URL,同时哈希函数的性质决定了同一条URL不可能分给不同的机器;或者在单机上将大文件通过哈希函数拆成1000个小文件,对每一个小文件再利用哈希表遍历,找出重复的URL;或者在分给机器或拆完文件之后,进行排序,排序过后再看是否有重复的URL出现。总之,牢记一点,很多大数据问题都离不开分流,要么是哈希函数把大文件的内容分配给不同的机器,要么是哈希函数把大文件拆成小文件,然后处理每一个小数量的集合。
5、TOPK搜索(小根堆)
海量搜索词汇,找到最热TOP100词汇的方法
最开始还是用哈希分流的思路来处理,把包含百亿数据量的词汇文件分流到不同的机器上,具体多少台机器由面试官规定或者由更多的限制来决定。对每一台机器来说,如果分到的数据量依然很大,比如,内存不够或其他问题,可以再用哈希函数把每台机器的分流文件拆成更小的文件处理。
处理每一个小文件的时候,哈希表统计每种词及其词频,哈希表记录建立完成后,再遍历哈希表,遍历哈希表的过程中使用大小为100的小根堆来选出每一个小文件的top 100(整体未排序的top 100)。每一个小文件都有自己词频的小根堆(整体未排序的top 100),将小根堆里的词按照词频排序,就得到了每个小文件的排序后top 100。然后把各个小文件排序后的top 100进行外排序或者继续利用小根堆,就可以选出每台机器上的top 100。不同机器之间的top100再进行外排序或者继续利用小根堆,最终求出整个百亿数据量中的top 100。对于top K 的问题,除哈希函数分流和用哈希表做词频统计之外,还经常用堆结构和外排序的手段进行处理。
6、中位数(单向二分查找)
10MB内存,找到100亿整数的中位数
①内存够:内存够还慌什么啊,直接把100亿个全部排序了,你用冒泡都可以...然后找到中间那个就可以了。但是你以为面试官会给你内存??
②内存不够:题目说是整数,我们认为是带符号的int,所以4字节,占32位。
假设100亿个数字保存在一个大文件中,依次读一部分文件到内存(不超过内存的限制),将每个数字用二进制表示,比较二进制的最高位(第32位,符号位,0是正,1是负),如果数字的最高位为0,则将这个数字写入 file_0文件中;如果最高位为 1,则将该数字写入file_1文件中。
从而将100亿个数字分成了两个文件,假设 file_0文件中有 60亿 个数字,file_1文件中有 40亿 个数字。那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10亿 个数字。(file_1中的数都是负数,file_0中的数都是正数,也即这里一共只有40亿个负数,那么排序之后的第50亿个数一定位于file_0中)
现在,我们只需要处理 file_0 文件了(不需要再考虑file_1文件)。对于 file_0 文件,同样采取上面的措施处理:将file_0文件依次读一部分到内存(不超内存限制),将每个数字用二进制表示,比较二进制的 次高位(第31位),如果数字的次高位为0,写入file_0_0文件中;如果次高位为1,写入file_0_1文件 中。
现假设 file_0_0文件中有30亿个数字,file_0_1中也有30亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第10亿个数字。
抛弃file_0_1文件,继续对 file_0_0文件 根据 次次高位(第30位) 划分,假设此次划分的两个文件为:file_0_0_0中有5亿个数字,file_0_0_1中有25亿个数字,那么中位数就是 file_0_0_1文件中的所有数字排序之后的 第 5亿 个数。
按照上述思路,直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。
7、短域名系统(缓存)
设计短域名系统,将长URL转化成短的URL.
(1)利用放号器,初始值为0,对于每一个短链接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),比如第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。
(2)将短链接服务器域名与放号器的62进制值进行字符串连接,即为短链接的URL,比如:t.cn/sBc。
(3)重定向过程:生成短链接之后,需要存储短链接到长链接的映射关系,即sBc -> URL,浏览器访问短链接服务器时,根据URL Path取到原始的链接,然后进行302重定向。映射关系可使用K-V存储,比如Redis或Memcache。
8、海量评论入库(消息队列)
假设有这么一个场景,有一条新闻,新闻的评论量可能很大,如何设计评论的读和写
前端页面直接给用户展示、通过消息队列异步方式入库
读可以进行读写分离、同时热点评论定时加载到缓存
9、在线/并发用户数(Redis)
显示网站的用户在线数的解决思路
维护在线用户表
使用Redis统计
显示网站并发用户数
- 每当用户访问服务时,把该用户的 ID 写入ZSORT队列,权重为当前时间
- 根据权重(即时间)计算一分钟内该机构的用户数Zrange
- 删掉一分钟以上过期的用户Zrem
10、热门字符串(前缀树)
假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。)
HashMap 法
虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4 表示整数占用的 4 个字节)。由此可见,1G 的内存空间完全够用。
思路如下:
首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 O(N) 。
接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。
遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 O(Nlog10) 。
前缀树法
当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。
思路如下:
在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。
最后依然使用小顶堆来对字符串的出现次数进行排序。
11、红包算法
线性切割法,一个区间切N-1刀。越早越多
二倍均值法,【0 ~ 剩余金额 / 剩余人数 * 2】中随机,相对均匀
11、手写快排
快排核心思想就是:首先在待排序数组中随便选择一个数作为节点(pivot),然后从最后面(high)往左查找比这个节点(pivot)小的数,并且从最前面(low)往右查找比这个节点(pivot)大的数(low),情况1:找到后就把这两个数进行交换,然后接着上面的查找交换直到low等于high,球后将节点(pivot)与low位置处的数进行交换,这样比pivot小的数都在其前面,比pivot大的数就在其后面,然后把数组以pivot分为两半,重复上述操作;情况2:直到low 等于 high都没有找到,就直接交换pivot和low位置的数据,然后同样的将数组以pivot分为两半重复上述操作。(说的挺抽象的,还是看看下面举例吧)
int[] arr = {1, 2, 3, 4, 5};
public class QuickSort {
//快排实现方法
public static void quickRow(int[] array, int low, int high){
int i,j,pivot;
//结束条件
if (low >= high) {
return;
}
i = low;
j = high;
//选择的节点,这里选择的数组的第一数作为节点
pivot = array[low];
while (i < j){
//从右往左找比节点小的数,循环结束要么找到了,要么i=j
while (array[j] >= pivot && i < j){
j--;
}
//从左往右找比节点大的数,循环结束要么找到了,要么i=j
while (array[i] <= pivot && i < j){
i++;
}
//如果i!=j说明都找到了,就交换这两个数
if (i < j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
//i==j一轮循环结束,交换节点的数和相遇点的数
array[low] = array[i];
array[i] = pivot;
//数组“分两半”,再重复上面的操作
quickRow(array,low,i - 1);
quickRow(array,i + 1,high);
}
//测试
public static void main(String[] args) {
int[] array = {6,3,7,1,9,4,8,5,2,10};
int low = 0,high = array.length - 1;
quickRow(array,low,high);
for (int i : array){
System.out.println(i);
}
}
}
public class QuickSort {
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/* 常规快排 */
public static void quickSort1(int[] arr, int L , int R) {
if (L > R) return;
int M = partition(arr, L, R);
quickSort1(arr, L, M - 1);
quickSort1(arr, M + 1, R);
}
public static int partition(int[] arr, int L, int R) {
if (L > R) return -1;
if (L == R) return L;
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R])
swap(arr, index, ++lessEqual);
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
/* 荷兰国旗 */
public static void quickSort2(int[] arr, int L, int R) {
if (L > R) return;
int[] equalArea = netherlandsFlag(arr, L, R);
quickSort2(arr, L, equalArea[0] - 1);
quickSort2(arr, equalArea[1] + 1, R);
}
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) return new int[] { -1, -1 };
if (L == R) return new int[] { L, R };
int less = L - 1;
int more = R;
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else {
swap(arr, index, --more);
}
}
swap(arr, more, R);
return new int[] { less + 1, more };
}
// for test
public static void main(String[] args) {
int testTime = 1;
int maxSize = 10000000;
int maxValue = 100000;
boolean succeed = true;
long T1=0,T2=0;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
int[] arr3 = copyArray(arr1);
// int[] arr1 = {9,8,7,6,5,4,3,2,1};
long t1 = System.currentTimeMillis();
quickSort1(arr1,0,arr1.length-1);
long t2 = System.currentTimeMillis();
quickSort2(arr2,0,arr2.length-1);
long t3 = System.currentTimeMillis();
T1 += (t2-t1);
T2 += (t3-t2);
if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) {
succeed = false;
break;
}
}
System.out.println(T1+" "+T2);
// System.out.println(succeed ? "Nice!" : "Oops!");
}
private static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random())
- (int) (maxValue * Math.random());
}
return arr;
}
private static int[] copyArray(int[] arr) {
if (arr == null) return null;
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
private static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null))
return false;
if (arr1 == null && arr2 == null)
return true;
if (arr1.length != arr2.length)
return false;
for (int i = 0; i < arr1.length; i++)
if (arr1[i] != arr2[i])
return false;
return true;
}
private static void printArray(int[] arr) {
if (arr == null)
return;
for (int i = 0; i < arr.length; i++)
System.out.print(arr[i] + " ");
System.out.println();
}
}
12、手写归并
import java.util.Arrays;
import java.util.Scanner;
public class xishuarr {
public static void main(String[] args) {
int arr[]= {8,4,5,7,1,3,6,2};
int[] add=new int[arr.length];
System.out.println("排序前:"+Arrays.toString(arr));
System.out.println("排序过程:");
mergeSort(arr,0,add.length-1,add);
System.out.println("排序后:"+Arrays.toString(arr));
}
/**
* 分
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void mergeSort(int[] arr,int left,int right,int[] temp) {
//求中间索引
int mid=(left+right)/2;
if(left<right) {
//左边递归分解
mergeSort(arr,left,mid,temp);
//右边递归分解
mergeSort(arr,mid+1,right,temp);
// 经过左边递归分解和右边递归分解之后,传入的数组变为下面左右两个都是有序的小数组;
//传入数组{4 ,5 ,7 ,8,1 ,2, 3, 6}
merge(arr,left,mid,right,temp);
System.out.println(" 最左边索引:"+left+"\t最右边边索引:"+right+"\t"+Arrays.toString(arr));
}
}
/**
*
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
//4 5 7 8 1 2 3 6
public static void merge(int[] arr,int left,int mid,int right,int[] temp) {
int i=left; //初始i,左边有序序列的初始索引
int j=mid+1; //初始j,右边有序序列的初始索引
int t=0; //指向temp数组的当前索引
//1.
//先把左右两边(有序)的数据按照规则填充到temp数组
//直到左右两边的有序序列,有一边处理完毕为止
while(i<=mid&&j<=right) {
if(arr[i]<arr[j]) {
temp[t]=arr[i++];
/**
* 这里我们这里是:temp[t]=arr[i++];
* 如果不好理解,你可以写成这样:
* temp[t]=arr[i];i++;
*/
}
else {
temp[t]=arr[j++];
}
//因为无论执行if里面的语句还是else里面的语句,t都要加1,所以把t移出来.
t++;
}
//2.
//把有剩余数据的一边的的数据依次全部填充到temp
//由上述循环条件:i<=mid&&j<=right 可知
//此时要么i>mid 要么j>right
while(i<=mid) {
temp[t]=arr[i];
t++;
i++;
}
while(j<=right) {
temp[t]=arr[j];
t++;
j++;
}
//3.
//把temp的数组转移到arr上
int n=0;
int tempLeft=left;
while(tempLeft<=right){
arr[tempLeft]=temp[n];
n++;
tempLeft++;
}
}
}
13、手写堆排
//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
//将堆顶元素与末尾元素交换,将最大元素“沉”到数组末端
//重新调整结构使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
private static void heapSort(int[] arr) {
int temp = 0;
//堆排序
for(int i = arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
for (int j=arr.length-1;j>0;j--){
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr,0,j);
}
System.out.println("数组="+Arrays.toString(arr));
}
/**
* 将以i对应的非叶子节点的树调整成一个大顶堆
* 举例 int[] arr = {4,6,8,5,9};=>i=1 =>{4,9,8,5,6} => i=0 =>{9,6,8,5,4}
* @param arr
* @param i 表示非叶子节点在数组中的索引
* @param length 对多少个元素进行调整
*/
public static void adjustHeap(int[] arr,int i,int length){
//a[i]>a[2i+1]&&a[i]>a[2i+2]
int temp = arr[i];
for (int k=i*2+1;k<length;k=k*2+1){
//先比较左子节点和右子节点的大小,最大的那个和temp进行交换
if(k+1<length && arr[k]<arr[k+1]){
k++;//k指向右子节点
}
//如果非子节点的值小于左子节点和右子节点的值
if(arr[k]>temp){
//temp和arr[k]进行交换
arr[i] = arr[k];
i=k;//继续循环比较,假设k是左子节点,k+1是右子节点,然后引出公式
}else{
break;
}
}
//当for循环结束后,我们已经将以i为父节点的树的最大值,放在了最顶上(局部)
arr[i]=temp;
}
14、手写单例
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
15、手写LRUcache
1、LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的;HashMap无序;2、LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。
// 基于linkedHashMap
public class LRUCache {
private LinkedHashMap<Integer,Integer> cache;
private int capacity; //容量大小
public LRUCache(int capacity) {
cache = new LinkedHashMap<>(capacity);
this.capacity = capacity;
}
public int get(int key) {
//缓存中不存在此key,直接返回
if(!cache.containsKey(key)) {
return -1;
}
int res = cache.get(key);
cache.remove(key); //先从链表中删除
cache.put(key,res); //再把该节点放到链表末尾处
return res;
}
public void put(int key,int value) {
if(cache.containsKey(key)) {
cache.remove(key); //已经存在,在当前链表移除
}
if(capacity == cache.size()) {
//cache已满,删除链表头位置
Set<Integer> keySet = cache.keySet();
Iterator<Integer> iterator = keySet.iterator();
cache.remove(iterator.next());
}
cache.put(key,value); //插入到链表末尾
}
}
// 双向链表节点类
class Node {
int key;
int value;
Node prev;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
// LRU缓存类
class LRUCache {
private int capacity;
private Map<Integer, Node> cache;
private Node head;
private Node tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
}
public int get(int key) {
if (cache.containsKey(key)) {
Node node = cache.get(key);
moveToHead(node);
return node.value;
}
return -1;
}
public void put(int key, int value){
if (cache.containsKey(key)) {
Node node = cache.get(key);
node.value = value;
moveToHead(node);
} else {
Node newNode = new Node(key, value);
cache.put(key, newNode);
addToHead(newNode);
if (cache.size() > capacity) {
Node tailNode = removeTail();
cache.remove(tailNode.key);
}
}
}
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
private void addToHead(Node node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private Node removeTail() {
Node tailNode = tail.prev;
removeNode(tailNode);
return tailNode;
}
}
16、手写线程池
package com.concurrent.pool;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class MySelfThreadPool {
//默认线程池中的线程的数量
private static final int WORK_NUM = 5;
//默认处理任务的数量
private static final int TASK_NUM = 100;
private int workNum;//线程数量
private int taskNum;//任务数量
private final Set<WorkThread> workThreads;//保存线程的集合
private final BlockingQueue<Runnable> taskQueue;//阻塞有序队列存放任务
public MySelfThreadPool() {
this(WORK_NUM, TASK_NUM);
}
public MySelfThreadPool(int workNum, int taskNum) {
if (workNum <= 0) workNum = WORK_NUM;
if (taskNum <= 0) taskNum = TASK_NUM;
taskQueue = new ArrayBlockingQueue<>(taskNum);
this.workNum = workNum;
this.taskNum = taskNum;
workThreads = new HashSet<>();
//启动一定数量的线程数,从队列中获取任务处理
for (int i=0;i<workNum;i++) {
WorkThread workThread = new WorkThread("thead_"+i);
workThread.start();
workThreads.add(workThread);
}
}
public void execute(Runnable task) {
try {
taskQueue.put(task);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void destroy() {
System.out.println("ready close thread pool...");
if (workThreads == null || workThreads.isEmpty()) return ;
for (WorkThread workThread : workThreads) {
workThread.stopWork();
workThread = null;//help gc
}
workThreads.clear();
}
private class WorkThread extends Thread{
public WorkThread(String name) {
super();
setName(name);
}
@Override
public void run() {
while (!interrupted()) {
try {
Runnable runnable = taskQueue.take();//获取任务
if (runnable !=null) {
System.out.println(getName()+" readyexecute:"+runnable.toString());
runnable.run();//执行任务
}
runnable = null;//help gc
} catch (Exception e) {
interrupt();
e.printStackTrace();
}
}
}
public void stopWork() {
interrupt();
}
}
}
package com.concurrent.pool;
public class TestMySelfThreadPool {
private static final int TASK_NUM = 50;//任务的个数
public static void main(String[] args) {
MySelfThreadPool myPool = new MySelfThreadPool(3,50);
for (int i=0;i<TASK_NUM;i++) {
myPool.execute(new MyTask("task_"+i));
}
}
static class MyTask implements Runnable{
private String name;
public MyTask(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("task :"+name+" end...");
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "name = "+name;
}
}
}
17、手写消费者生产者模式
阻塞队列
public class MyBlockingQueue {
private int maxSize;
private LinkedList<Integer> queue;
public MyBlockingQueue(int size) {
this.maxSize = size;
queue = new LinkedList<Integer>();
}
public synchronized void put() throws InterruptedException{
while(queue.size() == maxSize) {
System.out.println("队列已满,生产者:" + Thread.currentThread().getName() + "进入等待");
wait();
}
Random random = new Random();
int i = random.nextInt(100);
System.out.println("队列未满,生产者:"+Thread.currentThread().getName() + "放入数据" + i);
if(queue.size() == 0) {
notifyAll();
}
queue.add(i);
}
public synchronized void take() throws InterruptedException{
while(queue.size() == 0) {
System.out.println("队列为空,消费者:" + Thread.currentThread().getName() + "进入等待");
wait();
}
if(queue.size() == maxSize) {
notifyAll();
}
System.out.println("队列有数据,消费者:"+Thread.currentThread().getName() + "取出数据" + queue.remove());//删除第一个数据,最早放入的数据
}
}
生产者线程
public class Producer implements Runnable{
private MyBlockingQueue myBlockingQueue;
public Producer(MyBlockingQueue myBlockingQueue) {
this.myBlockingQueue = myBlockingQueue;
}
public void run() {
for(int i = 0 ; i < 5 ; i++) {
try {
myBlockingQueue.put();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者线程
public class Consumer implements Runnable{
private MyBlockingQueue myBlockingQueue;
public Consumer(MyBlockingQueue myBlockingQueue) {
this.myBlockingQueue = myBlockingQueue;
}
public void run() {
for(int i = 0 ; i < 5 ; i++) {
try {
myBlockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
18、手写阻塞队列
什么是阻塞队列
首先,阻塞队列是一个队列,满足队列的基本数据结构,先进先出。其次,当队列满时,队列会阻塞插入元素的线程,直到队列不满;当队列空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。
如何写一个阻塞队列
手写阻塞队列是多线程面试中常见的问题,能考察面试者对多线程和锁的基础知识。
通过synchronized关键字配合wait()和notify()方法,实现线程的交替运行:
对代码的一些解释
1、private final Queue<String> myQueue = new LinkedList<>();这样创建的队列实际上偷懒了,并不是完全用数组实现的队列,用了封装的队列,但毕竟重点不在于此。
2、使用了JUC包的CyclicBarrier类,通过调用await()方法,实现两个线程都执行完毕后,再执行相关代码,这不属于阻塞队列的范畴。
3、synchronized块中使用的对象锁不一定要用myQueue,使用同一个对象就行。
4、new Thread(()->{...},"生产者").start();是java8函数式写法,等同于继承Runnable覆写run方法。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.CyclicBarrier;
public class MyBlockingQueue {
//队列
private final Queue<String> myQueue = new LinkedList<>();
//最大长度
private static final int MAXSIZE = 20;
private static final int MINSIZE = 0;
//获取队列长度
public int getSize(){
return myQueue.size();
}
//生产者
public void push(String str) throws Exception {
//拿到对象锁
synchronized (myQueue){
//如果队列满了,则阻塞
while(getSize() == MAXSIZE){
myQueue.wait();
}
myQueue.offer(str);
System.out.println(Thread.currentThread().getName() + "放入元素" + str);
//唤醒消费者线程,消费者和生产者自己去竞争锁
myQueue.notify();
}
}
//消费者
public String pop() throws Exception {
synchronized (myQueue){
String result = null;
//队列为空则阻塞
while(getSize() == MINSIZE){
myQueue.wait();
}
//先进先出
result = myQueue.poll();
System.out.println(Thread.currentThread().getName()+"取出了元素" + result);
//唤醒生产者线程,消费者和生产者自己去竞争锁
myQueue.notify();
return result;
}
}
public static void main(String args[]){
MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
//两个线程,都执行完成了打印
CyclicBarrier barrier = new CyclicBarrier(2, ()->{
System.out.println("生产结束,下班了,消费者明天再来吧!");
});
//生产者线程
new Thread(()->{
//50个辛勤的生产者循环向队列中添加元素
try {
for(int i = 0; i < 50; i++){
myBlockingQueue.push("——" + i );
}
//生产完了
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},"生产者").start();
//消费者线程
new Thread(()->{
//50个白拿的消费者疯狂向队列中获取元素
try {
for(int j = 0; j < 50; j++){
myBlockingQueue.pop();
}
//消费完了
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},"消费者").start();
}
}
19、手写多线程交替打印ABC
使用同步块和wait、notify的方法控制三个线程的执行次序。具体方法如下:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能进行打印操作。一个对象锁是prev,就是前一个线程所对应的对象锁,其主要作用是保证当前线程一定是在前一个线程操作完成后(即前一个线程释放了其对应的对象锁)才开始执行。还有一个锁就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁(也就前一个线程要释放其自身对象锁),然后当前线程再申请自己对象锁,两者兼备时打印。之后首先调用self.notify()唤醒下一个等待线程(注意notify不会立即释放对象锁,只有等到同步块代码执行完毕后才会释放),再调用prev.wait()立即释放prev对象锁,当前线程进入休眠,等待其他线程的notify操作再次唤醒。
public class ABC_Synch {
public static class ThreadPrinter implements Runnable {
private String name;
private Object prev;
private Object self;
private ThreadPrinter(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {// 多线程并发,不能用if,必须使用whil循环
synchronized (prev) { // 先获取 prev 锁
synchronized (self) {// 再获取 self 锁
System.out.print(name);// 打印
count--;
self.notifyAll();// 唤醒其他线程竞争self锁,注意此时self锁并未立即释放。
}
// 此时执行完self的同步块,这时self锁才释放。
try {
if (count == 0) {// 如果count==0,表示这是最后一次打印操作,通过notifyAll操作释放对象锁。
prev.notifyAll();
} else {
prev.wait(); // 立即释放 prev锁,当前线程休眠,等待唤醒
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ThreadPrinter pa = new ThreadPrinter("A", c, a);
ThreadPrinter pb = new ThreadPrinter("B", a, b);
ThreadPrinter pc = new ThreadPrinter("C", b, c);
new Thread(pa).start();
Thread.sleep(100);// 保证初始ABC的启动顺序
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
可以看到程序一共定义了a,b,c三个对象锁,分别对应A、B、C三个线程。A线程最先运行,A线程按顺序申请c,a对象锁,打印操作后按顺序释放a,c对象锁,并且通过notify操作唤醒线程B。线程B首先等待获取A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C。线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程必须按照A,B,C的顺序来启动,但是这种假设依赖于JVM中线程调度、执行的顺序。
wait和notify操作的异同:
wait() 与 notify/notifyAll() 是Object类的方法,在执行两个方法时,要先获得锁。
当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。
当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。
从这里可以看出,notify/notifyAll()执行后,并不立即释放锁,而是要等到执行完临界区中代码后,再释放。所以在实际编程中,我们应该尽量在线程调用notify/notifyAll()后,立即退出临界区。即不要在notify/notifyAll()后面再写一些耗时的代码。
20、交替打印FooBar
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}
public class ClassForTestFoobar {
private int n;
public ClassForTestFoobar(int n){
this.n = n;
}
Semaphore semaphore1 = new Semaphore(1);
Semaphore semaphore2 = new Semaphore(0);
public void foo(Runnable printFoo) throws InterruptedException {
for(int i=0;i<n;i++){
semaphore1.acquire();
printFoo.run();
semaphore2.release();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for(int i=0;i<n;i++){
semaphore2.acquire();
printBar.run();
semaphore1.release();
}
}
public static void main(String[] args) {
ClassForTestFoobar classForTestFoobar = new ClassForTestFoobar(3);
Thread thread1 = new Thread(){
@Override
public void run() {
try {
classForTestFoobar.foo(
new Runnable() {
@Override
public void run() {
System.out.print("foo");
}
}
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
try {
classForTestFoobar.bar(
new Runnable() {
@Override
public void run() {
System.out.print("bar");
}
}
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread2.start();
thread1.start();
}
}
public class ClassForTestFoobar {
private int n;
public ClassForTestFoobar(int n){
this.n = n;
}
private volatile boolean flag = false;
private Object object = new Object();
public void foo(Runnable printFoo) throws InterruptedException {
for(int i=0;i<n;i++){
synchronized (object){
// bar输出时等待
if(flag){
object.wait();
}
// 输出foo
printFoo.run();
// 唤醒bar线程
object.notify();
// 标识唤醒bar输出
flag = true;
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for(int i=0;i<n;i++){
synchronized (object){
// 等待foo输出
if(!flag){
object.wait();
}
// 输出bar
printBar.run();
// 唤醒foo线程
object.notify();
// 控制标识,控制foo输出
flag = false;
}
}
}
public static void main(String[] args) {
ClassForTestFoobar classForTestFoobar = new ClassForTestFoobar(3);
Thread thread1 = new Thread(){
@Override
public void run() {
try {
classForTestFoobar.foo(
new Runnable() {
@Override
public void run() {
System.out.print("foo");
}
}
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
try {
classForTestFoobar.bar(
new Runnable() {
@Override
public void run() {
System.out.print("bar");
}
}
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread2.start();
thread1.start();
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY