网上的各种关于 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 教程来学一学也是可以的。

拿微博来说吧,比如我准备采集某领域的 100 个大 V 的微博内容,如果用单线程来爬,可能耗时几十分钟,甚至几个小时。如果是更大量的数据采集,那耗时可想而知了。

为了加快速度、提高效率,一般都会采用多线程的方式来爬取数据。

不仅如此,爬虫程序还是 生产消费者模式 的经典应用场景。

整体的逻辑如下:

1、多个线程(这里的线程也就是生产消费者模式中的生成者线程)到目标网站上抓数据;

2、然后将数据放到一个中间队列,有条件的可以弄个真正的消息队列,比如 RabbitMQ、RocketMQ、kafka 或者 redis 也可以,没有条件的用 Java 本身的队列 Queue 也可以,或者自己实现一个队列结构;

3、最后,多个消费者线程订阅中间队列,将数据加工处理后存入数据库或者写入文件,实现持久化。

涉及到多线程的知识点

线程的创建

爬虫生产者线程和消费线程的创建,可以采用线程创建的两种方式:

1、实现 Runnable 接口,并重写 run() 方法

public static void main( String[] args ){
    ProducerWorker producerWorker = new ProducerWorker();
    producerWorker.run();
}
public static class ProducerWorker implements Runnable {
    @Override
    public void run(){
        try {
            /* 抓取数据 */
        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }
}

2、继承 Thread 类,重写 run() 方法

public static void main( String[] args ){
    ProducerWorker producerWorker = new ProducerWorker();
    Thread thread = new Thread( producerWorker );
    thread.start();
}
public static class ProducerWorker extends Thread {
    @Override
    public void run(){
        try {
            /* 抓取数据 */
        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }
}

除了手动创建线程的方法,还可以使用线程池的方式启动线程,比如我们打算抓取100个大 V 的微博信息,但是单台机器同时启动 100 个线程也不现实,线程数并不是越多越好,主要看机器的 CPU 核数和业务类型(CPU 密集型还是 IO 密集型),很显然我们这个爬虫是 CPU 密集型,一般线程数设置成 CPU 核数或者比 CPU 核数多1就可以。

假设我们设置生产者线程池大小为 4 ,然后利用这个线程池开始执行抓取任务,这里的关键是等待队列和 ThreadPoolExecutor.DiscardOldestPolicy 策略配合使用,ThreadPoolExecutor.DiscardOldestPolicy 的策略是后来的进程进入等待队列,如果超过队列长度,再有进程进来,就舍弃最早进入的线程。这里直接将等待队列设置为 100,保证不会有任务被舍弃掉。

public class PoolThread {
   public static void main(String[] args){
       /**
        * 等待队列长度 100 ,配合 ThreadPoolExecutor.DiscardOldestPolicy 策略
        */
       BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
       NameThreadFactory threadFactory = new NameThreadFactory();
       ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 30, TimeUnit.SECONDS,
               workQueue,threadFactory,new ThreadPoolExecutor.DiscardOldestPolicy());
       for (int i = 1; i <= 100; i++) {d
           ProducerWorker producerWorker = new ProducerWorker(String.valueOf(i));
           executor.execute(producerWorker);
      }
  }
   static class NameThreadFactory implements ThreadFactory {
       private int counter = 1;
       @Override
       public Thread newThread(Runnable r) {
           Thread t = new Thread(r, "consumer-" + counter++);
           return t;
      }
  }
   public static class ProducerWorker implements Runnable{
       private String workerName;
       public ProducerWorker(String workerName){
           this.workerName = workerName;
      }
       @Override
       public void run() {
           try {
               // 抓取数据
               System.out.println("由线程-" + Thread.currentThread().getName() + "执行第"+this.workerName+"个任务");
          } catch (Exception e) {
               e.printStackTrace();
          }
      }
  }
}

共享数据加锁

当生产者线程将数据写入到共享队列的同,消费者线程就要开始工作了,消费线程会不断的在共享队列中取数据,这里不考虑使用第三方消息队列的方式,只说 Java 提供的 Queue 和我们自己实现的队列。

对于 Queue 的实现类 LinkedBlockingQueue 或者  ConcurrentLinkedQueue 都是线程安全的,使用的时候可以不考虑加锁、解锁的问题,因为它们内部已经帮我们实现好了。

如果是自己写的队列,那我们就要在获取对象的时候进行加锁操作,利用 synchronized 关键字或者可重入锁等。

总结

总之,多线程爬虫是学习 Java 多线程很好的场景,既能很好的练习,又没有那么枯燥。在做爬虫的过程中必然会遇到很多问题,有可能是多线程爬虫设计思路上的问题,有可能是具体的技术细节。爬虫场景越复杂,理解和掌握的就越深刻,当然难度也会相应的变大。

还等什么,正好现在闲在家里不让出门,利用空闲时间来做一个吧。

比如我,最近经常逛 B 站,准备写一个抓取 B 站大 V 名单的爬虫程序。

也欢迎大家和我一起来交流过程中遇到的问题和经验技巧,点击公众号右下角菜单可以添加微信好友进入交流群

还可以读:

从 volatile 说起,可见性和有序性是什么

Java 中的几种线程池不知道你用对了没有

系统内存爆满,原来是线程搞的鬼

多线程之---用 CountDownLatch 说明 AQS 的实现

-----------------------

公众号:古时的风筝

一个斜杠程序员,一个纯粹的技术公众号,多写 Java 相关技术文章,不排除会写其他内容。

【我就很喜欢呀!】

相关文章