前言
1.需求场景分析
在项目中,常常可以遇到一些可并发执行的操作,比如要做一个热搜系统,在用户搜索信息的同时,将用户的搜索记录插入到数据库当中;再比如用户在对数据做增删改操作的数据,系统同时记录用户的操作到数据库日志表当中,都是可以并发执行的过程。这时候我们就可以用多线程的方式实现并发,提高程序的运行效率。
2.为什么用线程池
使用线程池实现多线程,最大的原因就是线程池使得程序员可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统的运行压力。
一、线程池介绍
线程池中有许多准备运行的线程,每当为线程池提供一个Runnable,就会有一个线程调用run方法。当run方法退出时,这个线程不会死亡,而是留在池中准备为下一个请求提供服务。
执行器(Executors)类有许多静态工厂方法,用来构造线程池,主要包括以下方法:
二、需求案例与实现
例如,我们实现在用户搜索信息的同时,将用户的搜索记录插入到数据库当中的一个并发需求。
1.任务实现Runnable接口
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class insertRecordTask implements Runnable{
private TSearch search;
private TSearchMapper tSearchMapper;
public insertRecordTask(TSearch search, TSearchMapper tSearchMapper){
this.tSearchMapper = tSearchMapper;
this.search = search;
}
@Override
public void run() {
try{
// 这里用MybaytisPlus做插入操作
tSearchMapper.insert(search);
} catch (Exception e){
log.info("------------插入搜索记录失败------------");
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
2.在方法中实现需求
@Service
public class TSearchServiceImpl extends ServiceImpl<TSearchMapper, TSearch> implements TSearchService {
@Resource
TSearchMapper tSearchMapper;
// 根据需求选择适当的线程池和线程池大小
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public Result searchAndRec(TSearch search){
// 这里用MybaytisPlus对数据库做查找
QueryWrapper<TSearch> wrapper = new QueryWrapper<>();
wrapper.setEntity(search);
List<TSearch> resultList = baseMapper.selectList(wrapper)
// 将记录插入数据库的任务提交给线程执行
insertRecordTask record = new insertRecordTask(search, tSearchMapper);
executor.submit(record);
return Result.ok().result(resultList);
}
}
三、注意事项
1.线程安全问题
如果方法需要处理高并发场景的需求,且线程之间有共享变量,这种情况下要考虑线程安全问题,处理方法包括在run方法当中加锁等操作。
2.适当使用Redis
本文中展示了一个实现热搜系统的思路,如果系统有高访问量的需求,比如做国内某新闻的热搜排行,那么数据库是很难在短时间内经得起全国大规模用户访问带来的数据量的,所以要做好Redis缓存与数据库的读写同步,并且一定要防止Redis出现雪崩击穿等问题。