![f8d9bd58dbecaa9f793162a7acefc03a.png](https://i-blog.csdnimg.cn/blog_migrate/899ba2ad7c38ba03edbdfa0a5e92bafe.jpeg)
网上的各种关于 Java 学习路线的文章中都会提到多线程,往往作为 Java 进阶的部分存在。就是说当你想要在 Java 这条路上有所成,必须要掌握多线程。另外,在几乎所有的 Java 岗位的招聘要求中,都会提到多线程,如果你不能针对多线程说出个一二三,恐怕都找不到满意的工作。
但是,很多同学在工作过程中,好像并不会接触到多线程的东西啊。我们知道,像 tomcat 这种服务器软件本身就是多线程的,但是人家都已经封装好了,我们只需要拿来用即可,并不需要我们对多线程进行什么特殊的操作。
其实大多数的同学并不会直接在工作中使用多线程开发。那怎么办呢,我们总不能为了使用多线程而使用多线程吧。
那么什么场景下会使用到多线程技术呢。
多线程的使用场景
1、应用服务器软件
各种服务器软件都支持多线程的,比如我们常用的 Tomcat、Nginx、Jetty 等。因为线上服务肯定都不是一个人使用的,只要有多人访问,就有可能出现并发的情况,所以服务端软件必须是支持多线程的。
对于这些服务器软件,我们平时就是当做黑盒使用,最多也就是定制一下参数。很少有人有机会参与修改和开发。
2、游戏后端服务
如果你是从事游戏服务端开发的,游戏服务器一般都是定制开发的,我们的应用程序大多数都是 HTTP、HTTPS 协议,而游戏是一个长交互的过程,所以游戏中采用的通信协议一般都是 TCP、UDP 协议,如果是页游的话则可能是 websockets。而且游戏一般都是多人同时在线,必须要支持多线程、高并发。而且大多数的游戏后端服务都是采用 C++ 开发,很少有用 Java 开发的。
3、中间件、框架
中间件类似于连接器的效果,比如数据库中间件,用来连接我们的应用服务和数据库集群。
框架指的是那些封装好的拿来即用的框架,比如 RPC 框架,例如 Dubbo。或者是高性能的服务器框架 Netty。
由于面向的使用者未知,有可能是只有几十个人的内部应用,也有可能是成百上千万的超级应用,所以必须要考虑高并发、多线程的情况。
只有极少数的人需要开发中间件或者好用的框架,大多数人没有这个机会。
4、后台离线任务
后台离线任务一般是定期执行的 job,比如对于实时性要求不高的报表进行统计。
比如一个文件系统,每天要统计文件的增量信息,比如新增文件个数、文件空间占用等,我们如果用单一线程统计的话,当天新增文件较多的情况下统计就会很慢,这种情况就可以使用多线程统计,如果文件本身按照一定的规则分放的不同的文件夹里,可以针对每个文件夹创建一个线程进行扫描。
或者对于 mysql 数据表进行统计,比如对多个表进行汇总,每个表作为一个汇总指标存在,这样就可以针对每个表创建一个线程处理。或者有分库分表的情况,可以针对每个分表开启一个线程处理。
以上这些离线任务都可以通过多线程的方式加快处理速度。而这个场景在我们的项目中碰到的几率也比较大。
5、异步处理任务
异步处理任务类似于离线任务,同样是对实时性要求不是很高的情况下。比如电商系统中经常用到的短信、邮件通知的情况,用户在某电商网站下单付款后,通常会收到短信或者邮件的通知,而通知信息对于整个购买环节并不是最重要的,商家最关心的就是减掉库存和收到付款,所以对于通知的发送一般都采用异步方式,允许一定的延时甚至发送失败的情况。
当用户购买商品成功后,系统会向消息队列中写入订单相关的信息,发送通知的异步任务去消息队列拉取消息,拉到一个订单就向对应的手机或者邮箱发送通知消息。而这里的发送通知任务一般都采用多线程的方式,用来提高并发度,减小用户下单成功到收到通知之间的延时。
类似的场景都可以采用多线程的异步任务去处理,而我们在项目中碰到这种场景的几率也不小。
6、天然需要多线程的
另外还有一些功能就是一想到自然就想到多线程的情况,比较明显的就是爬虫程序。
通过爬虫学习多线程
如果你想学习多线程开发,但是又找不到合适的学习场景(对于照着书本或者博客敲一些 demo 代码并不能很好的掌握多线程开发),那么你可以试着写个爬虫玩玩,既有趣儿又可以学习,何乐而不为呢。
当然一提到写爬虫,大家可能最先想到的是 Python,没错,比如我在刚学习 Python 的前两年就热衷于写爬虫,比如 2014 年写的这篇 https://www.cnblogs.com/fengzheng/p/3913639.html ,抓取百度音乐,竟然还用 PYQT 做了个界面出来,也真是有耐心。
今天建议大家写爬虫来学习 Java 多线程,那就自然是用 Java 开发了。
这里只说单机多线程的爬虫,不包括分布式爬虫。
一般用爬虫就是需要大批量数据的场景,比如说抓取某个或某些微博大 V 的微博内容,比如抓取知乎回答,豆瓣电影排行信息,甚至到 PornHub 上抓一下 Python 教程来学一学也是可以的。
![778fc9cb0a34362cbd8170f0ad1f48e8.png](https://i-blog.csdnimg.cn/blog_migrate/37e47584590d9dce12468e8af25b226c.jpeg)
拿微博来说吧,比如我准备采集某领域的 100 个大 V 的微博内容,如果用单线程来爬,可能耗时几十分钟,甚至几个小时。如果是更大量的数据采集,那耗时可想而知了。
为了加快速度、提高效率,一般都会采用多线程的方式来爬取数据。
不仅如此,爬虫程序还是生产消费者模式的经典应用场景。
整体的逻辑如下:
1、多个线程(这里的线程也就是生产消费者模式中的生成者线程)到目标网站上抓数据;
2、然后将数据放到一个中间队列,有条件的可以弄个真正的消息队列,比如 RabbitMQ、RocketMQ、kafka 或者 redis 也可以,没有条件的用 Java 本身的队列 Queue 也可以,或者自己实现一个队列结构;
3、最后,多个消费者线程订阅中间队列,将数据加工处理后存入数据库或者写入文件,实现持久化。
![29cc6ed920c31d9f775e23c7c3b6da77.png](https://i-blog.csdnimg.cn/blog_migrate/38be052ec7dbb24e53125ca2d5203185.jpeg)
涉及到多线程的知识点
线程的创建
爬虫生产者线程和消费线程的创建,可以采用线程创建的两种方式:
1、实现 Runnable 接口,并重写 run() 方法
public
2、继承 Thread 类,重写 run() 方法
public
除了手动创建线程的方法,还可以使用线程池的方式启动线程,比如我们打算抓取100个大 V 的微博信息,但是单台机器同时启动 100 个线程也不现实,线程数并不是越多越好,主要看机器的 CPU 核数和业务类型(CPU 密集型还是 IO 密集型),很显然我们这个爬虫是 CPU 密集型,一般线程数设置成 CPU 核数或者比 CPU 核数多1就可以。
假设我们设置生产者线程池大小为 4 ,然后利用这个线程池开始执行抓取任务,这里的关键是等待队列和 ThreadPoolExecutor.DiscardOldestPolicy 策略配合使用,ThreadPoolExecutor.DiscardOldestPolicy 的策略是后来的进程进入等待队列,如果超过队列长度,再有进程进来,就舍弃最早进入的线程。这里直接将等待队列设置为 100,保证不会有任务被舍弃掉。
public
共享数据加锁
当生产者线程将数据写入到共享队列的同事,消费者线程就要开始工作了,消费线程会不断的在共享队列中取数据,这里不考虑使用第三方消息队列的方式,只说 Java 提供的 Queue 和我们自己实现的队列。
对于 Queue 的实现类 LinkedBlockingQueue 或者 ConcurrentLinkedQueue都是线程安全的,使用的时候可以不考虑加锁、解锁的问题,因为它们内部已经帮我们实现好了。
如果是自己写的队列,那我们就要在获取对象的时候进行加锁操作,利用 synchronized 关键字或者可重入锁等。
总结
总之,多线程爬虫是学习 Java 多线程很好的场景,既能很好的练习,又没有那么枯燥。在做爬虫的过程中必然会遇到很多问题,有可能是多线程爬虫设计思路上的问题,有可能是具体的技术细节。爬虫场景越复杂,理解和掌握的就越深刻,当然难度也会相应的变大。
还等什么,正好现在闲在家里不让出门,利用空闲时间来做一个吧。
比如我,最近经常逛 B 站,准备写一个抓取 B 站大 V 名单的爬虫程序。