前言
在同学的定时抓取博客的任务做好之后,进行的抓取的尝试,在抓取文章之后,本来计划的用lucene建立倒排索引,然后为每一篇抓取到的文章在所有的文章中,利用倒排索引计算TF-IDF,然后计算最相似的文章。当时收录了3500篇文章,对于每一篇文章都需要和其他的三千多篇文章计算相似度,非常的慢,所以想到使用多线程分别对文章进行相似文章的计算,然后提高运行的速度。
使用java的Thread类,手动创建线程
每一个线程,都会读取数据库文章列表中的一部分文章,通过传入的limit和offset来判断,然后这个线程就专心处理这一部分数据。
/**
* 进行计算的线程
*/
public class MyThread extends Thread {
public int limit;
public int offset;
public int id;
List<Article> articles;
public MyThread(int limit ,int offset,int id) {
this.limit = limit;
this.offset = offset;
this.id = id;
articles = articleDao.selectArticleList(limit,offset);
}
// 重写运行方法
@SneakyThrows
@Override
public void run() {
//计算文章的相似文章 存入redis
for(Article article :articles)
{
int articleId = article.getId();
System.out.println(id+"号线程"+"正在计算"+articleId+"号文章的相似文章");
List<Integer> similarityArticleList = luceneUtil.searchSimilarity(articleId,SIZE);
System.out.println(articleId+"的相似文章为"+similarityArticleList);
//移除旧的相似文章
Set<String> removeList = redisUtils.setGetValues(SET_KEY+String.valueOf(articleId));
for(String str : removeList)
{
redisUtils.setremove(SET_KEY+String.valueOf(articleId),str);
}
//加入新的相似文章
for (int i :similarityArticleList)
{
if(i != articleId)
{
redisUtils.setAdd(SET_KEY+String.valueOf(articleId) , String.valueOf(i));
}
}
}
}
}
主线程负责对数据进行分页,然后创建子线程,给每个子线程不同的页,分别进行计算,然后将相似文章的计算结果提前存入redis
/**
* 每次抽取完成,并且建立索引之后,都计算每篇文章10个相似的文章,存入redis,方便取用
*/
public void init() throws IOException, org.apache.lucene.queryParser.standard.parser.ParseException, InterruptedException {
//初始化
luceneUtil.postConstruct();
//从第几页开始 默认从0开始
int curPage=0;
//每次写入两条
int size=500;
//数据库中的文章总条数
int articleNumbers = articleDao.selectArticleCount();
int totalPage = articleNumbers/size;
System.out.println("总条数"+articleNumbers);
System.out.println("总页数"+totalPage);
List<MyThread> threads = new ArrayList<>();
while (curPage<=totalPage)
{
MyThread m1 = new MyThread(size,curPage*size,curPage);
m1.start();
threads.add(m1);
System.out.println("当前页数"+curPage);
curPage++;
}
// 将计算的线程合并到主线程中 等待子线程全部执行结束 主线程才结束
for (int i = 0; i < threads.size(); i++) {
threads.get(i).join();
}
System.out.println("执行结束");
}
问题:
刚开始的时候,主线程new完新的线程,然后调用了start()方法,将线程run了起来,之后就没在处理,最后发现主线程不会等待子线程结束,就直接会运行完整个方法。
是由于我在start子线程之后,没有对子线程进行join,join会让主线程等在子线程结束之后,在结束主线程
所我建了个List存储线程,最后进行join操作,让主线程等待。
// 将计算的线程合并到主线程中 等待子线程全部执行结束 主线程才结束
for (int i = 0; i < threads.size(); i++) {
threads.get(i).join();
}
最后的执行速度很快
不到5分钟计算完毕,才开了八个线程。
但是,自己创造线程也有一些问题:
线程是稀缺资源,不能频繁的创建。自己创建线程,很可能造成一些资源的浪费,所以好像更加推荐使用线程池。
SpringBoot配置和使用线程池
(1)线程池原理
threadPool.execute(new Job());
execute的执行流程:
(1)获取当前线程池的状态。
(2)当前线程数量小于 coreSize 时创建一个新的线程运行。
(3)如果当前线程处于运行状态,并且写入阻塞队列成功。
(4)双重检查,再次获取线程状态;如果线程状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断(5)线程是否全部执行完毕。同时执行拒绝策略。
(6)如果当前线程池为空就新创建一个线程并执行。
(7)如果在第三步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略。
SpringBoot 使用线程池:
(1)首先是配置线程池的bean交给spring 管理:
@Configuration
public class TaskExecutePool {
@Bean(name ="threadPoolA")
public ThreadPoolTaskExecutor TaskAsyncPool() {
System.out.println("线程池已创建");
ThreadPoolTaskExecutor executor =new ThreadPoolTaskExecutor();
executor.setCorePoolSize(15);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("Pool-A");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
corePoolSize:表示线程池核心线程,正常情况下开启的线程数量。
queueCapacity:当核心线程都在跑任务,还有多余的任务会存到此处。
maxPoolSize:如果queueCapacity存满了,还有任务就会启动更多的线程,直到线程数达到maxPoolSize。如果还有任务,则根据拒绝策略进行处理。
(2)使用
使用线程只需要在执行方法上加上注解,同时该方法的类必须被定义为bean,交由spring管理。
可以在类上使用注解@Component、@Service等
@Async(value="ThreadPoolA")
public void calculateSimilarity(int curPage,int size) throws IOException, ParseException {
List<Article> articles = articleDao.selectArticleList(size, curPage * size);
// 计算文章的相似文章 存入redis
for (Article article : articles) {
int articleId = article.getId();
System.out.println("正在计算" + articleId + "号文章的相似文章");
List<Integer> similarityArticleList = luceneUtil.searchSimilarity(articleId, SIZE);
System.out.println(articleId + "号相似文章味为"+similarityArticleList);
//移除旧的相似文章
Set<String> removeList = redisUtils.setGetValues(SET_KEY + String.valueOf(articleId));
for (String str : removeList) {
redisUtils.setremove(SET_KEY + String.valueOf(articleId), str);
}
// 加入新的相似文章
for (int i : similarityArticleList) {
if (i != articleId) {
redisUtils.setAdd(SET_KEY + String.valueOf(articleId), String.valueOf(i));
}
}
System.out.println("当前页数" + curPage);
}
}
/**
* 每次抽取完成,并且建立索引之后,都计算每篇文章10个相似的文章,存入redis,方便取用
*/
public void init() throws IOException, org.apache.lucene.queryParser.standard.parser.ParseException, InterruptedException {
//初始化
luceneUtil.postConstruct();
//从第几页开始 默认从0开始
int curPage=0;
//每次写入两条
int size=150;
//数据库中的文章总条数
int articleNumbers = articleDao.selectArticleCount();
int totalPage = articleNumbers/size;
System.out.println("总条数"+articleNumbers);
System.out.println("总页数"+totalPage);
while (curPage<=totalPage)
{
calculateSimilarity(curPage,size);
System.out.println("当前页数"+curPage);
curPage++;
}
System.out.println("执行结束");
}
最后的执行结果为:
虽然速度慢了写,但是他会自动调度资源,并且占用的内存,cpu都少,不会浪费资源。
SpringBoot配置和使用线程池(方法2)
(1)配置线程池
@Bean(name ="threadPoolB")
public ExecutorService ThreadPool() {
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("consumer-queue-thread-%d").build();
ExecutorService pool = new ThreadPoolExecutor(15, 20, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(10),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
return pool ;
}
(2)执行任务
/**
* 每次抽取完成,并且建立索引之后,都计算每篇文章10个相似的文章,存入redis,方便取用
*/
public void init() throws IOException, org.apache.lucene.queryParser.standard.parser.ParseException, InterruptedException {
//初始化
luceneUtil.postConstruct();
//从第几页开始 默认从0开始
int curPage=0;
//每次写入两条
int size=300;
//数据库中的文章总条数
int articleNumbers = articleDao.selectArticleCount();
int totalPage = articleNumbers/size;
System.out.println("总条数"+articleNumbers);
System.out.println("总页数"+totalPage);
CountDownLatch countDownLatch = new CountDownLatch(totalPage+1);
while (curPage<=totalPage)
{
MyThread m1 = new MyThread(size,curPage*size,curPage);
consumerQueueThreadPool.execute(m1);
/
System.out.println("当前页数"+curPage);
curPage++;
}
try {
//保证之前的所有的线程都执行完成,才会走下面的;
countDownLatch.await();
// 这样就可以在下面拿到所有线程执行完的集合结果
} catch (Exception e) {
System.out.println("阻塞异常");
}
System.out.println("执行结束");
}
/**
* 进行计算的线程
*/
public class MyThread extends Thread {
public int limit;
public int offset;
public int id;
List<Article> articles;
public MyThread(int limit ,int offset,int id) {
this.limit = limit;
this.offset = offset;
this.id = id;
articles = articleDao.selectArticleList(limit,offset);
}
// 重写运行方法
@SneakyThrows
@Override
public void run() {
// System.out.println("当前是"+id+"号线程");
// System.out.println("开始计算limit="+limit+" offset"+offset +"的数据");
//计算文章的相似文章 存入redis
for(Article article :articles)
{
int articleId = article.getId();
System.out.println(id+"号线程"+"正在计算"+articleId+"号文章的相似文章");
List<Integer> similarityArticleList = luceneUtil.searchSimilarity(articleId,SIZE);
// System.out.println(articleId+"的相似文章为"+similarityArticleList);
//移除旧的相似文章
Set<String> removeList = redisUtils.setGetValues(SET_KEY+String.valueOf(articleId));
for(String str : removeList)
{
redisUtils.setremove(SET_KEY+String.valueOf(articleId),str);
}
//加入新的相似文章
for (int i :similarityArticleList)
{
if(i != articleId)
{
redisUtils.setAdd(SET_KEY+String.valueOf(articleId) , String.valueOf(i));
}
}
}
}
}
(3)让子线程在执行完之前,阻塞主进程
CountDownLatch countDownLatch = new CountDownLatch(totalPage+1);
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
try {
//保证之前的所有的线程都执行完成,才会走下面的;
countDownLatch.await();
// 这样就可以在下面拿到所有线程执行完的集合结果
} catch (Exception e) {
System.out.println("阻塞异常");
}