18

工作中没有多线程使用场景,怎么办

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxMjA0MDk2OA%3D%3D&%3Bmid=2449469280&%3Bidx=1&%3Bsn=c4bcec1d02e2b3932c95a5ee2f1b9191
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

Qvaymeu.jpg!web

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

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

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

整体的逻辑如下:

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

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

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

MzQV3a3.jpg!web

涉及到多线程的知识点

线程的创建

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

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 相关技术文章,不排除会写其他内容。

qme6RfA.jpg!web

【我就很喜欢呀!】

BBR7riA.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK