zoukankan      html  css  js  c++  java
  • 1.多线程说在前面的话

    1.前言

    相信大家在初学java的时候多多少少都是学习过多线程的,但是对这个知识会有不少疑惑,在这总结一下:

    1.多线程在初学的时候不太好学,并且一般写项目的时候也很少用得上(至少在初学阶段时写的项目基本不需要自己创建线程)。

    2.多线程的知识点在面试经常考,多线程所涉及的知识点非常多,难度也不低。

    这破玩意涉及的东西是真的广,平时也不怎么用,怎么面试就偏偏爱问这个鬼东西

    2.为什么使用多线程?

    首先,我们要明确的是,为什仫要使用多线程?,有人会回答使用多线程就是为了加快程序运行的速度啊,那多线程是如何加快程序运行的速度呢?
    答:于我的理解:使用多线程最主要的原因是提高系统的资源利用率
    随着硬件的进步以及需求的扩大,现在的电脑CPU多为多核,如果你只是使用单线程,那么就是只用到一个核心,其他的核心就相当于空闲在那里。
    比如:厕所的坑位有5个,如果只用一个坑位,那不是很亏?比如现在我有5个人要上厕所。
    在单线程的时候:进去一个人解决要10分钟,然后后面的人都得等一个坑位。那总的时间就要花费50分钟。
    在多线程的时候,进去一个人要解决10分钟,然后后面的人发现还有别的坑位,就去别的坑位了,不是傻瓜地等一个坑位。

    我们可以把「等坑位」看作是IO操作,众所周知IO操作相对于CPU而言是非常慢的,CPU等待IO那段时间是空闲的。如果我们需要做类似IO这种慢的操作,可以开多个线程出来,尽量不要让CPU空闲下来,提高系统的资源利用率。说白了就是压榨CPU的资源,充分利用闲置的资源。

    多线程并不是说线程越多,我们的资源利用效率就越好,执行IO操作我们线程可以适当多一点,因为很多时候CPU是相对空闲的,如果是计算型的操作,本来CPU就不空闲了,还开很多的线程就不对了(有多线程就会有线程切换的问题,线程切换都是需要耗费资源的)所以线程的多少要适合CPU核数。

    3.多线程离我们远吗?

    多线程其实离我们很近,只是多数情况我们不知道也没有去刻意去意识到。
    Tomcat我相信每个Java后端的同学都认识它,它就是以多线程去响应请求的,我们可以在server.xml中配置连接池的配置,比如:

    <Connector 
    port="8080" 
    maxThreads="350" 
    maxHttpHeaderSize="8192" 
    minSpareThreads="45" 
    maxPostSize="512000" 
    protocol="HTTP/1.1" 
    enableLookups="false" 
    redirectPort="8443" 
    acceptCount="200" 
    keepAliveTimeout="15000" 
    maxKeepAliveRequests="-1" 
    maxConnections="25000" 
    connectionTimeout="15000" 
    disableUploadTimeout="false" 
    useBodyEncodingForURI="true" 
    URIEncoding="UTF-8" />
    

    Tomcat处理每一个请求都会从线程连接池里边用一个线程去处理,这显然是多线程的操作,然后这个请求线程顺藤摸瓜到了我们的Servlet,执行对应的service()方法。

    而我们的service方法是无状态的,多个线程请求service方法,往往都没有操作共享变量,不操作共享变量就不会有线程安全问题。

    上面只是用了Servlet举例,我们常用的SpringMVC其实也是一样的(毕竟底层还是Servlet)。
    还有我们在连接数据库的时候,也会用对应的连接池(Druid、C3P0、DBCP等),比如常见的Druid配置:

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> 
         <property name="url" value="${jdbc_url}" />
         <property name="username" value="${jdbc_user}" />
         <property name="password" value="${jdbc_password}" />
         <property name="filters" value="stat" />
         <property name="maxActive" value="20" />
         <property name="initialSize" value="1" />
         <property name="maxWait" value="60000" />
         <property name="minIdle" value="1" />
         <property name="timeBetweenEvictionRunsMillis" value="60000" />
         <property name="minEvictableIdleTimeMillis" value="300000" />
         <property name="testWhileIdle" value="true" />
         <property name="testOnBorrow" value="false" />
         <property name="testOnReturn" value="false" />
         <property name="poolPreparedStatements" value="true" />
         <property name="maxOpenPreparedStatements" value="20" />
         <property name="asyncInit" value="true" />
     </bean>
    

    我想说的是:我们日常开发的程序几乎都是多线程模式的,只是绝大多数时候我们没感知到而已。很多时候都是框架帮我们屏蔽掉了。

    4.多线程知识重要吗?

    首先我要明确,多线程很重要
    相信后端的学生有很多都知道,多线程是面试官百试不爽的面试题,在这里我们就先了解多线程必须掌握的知识:
    1.线程和进程的区别
    2.Thead类的常见方法
    3.可以用什么手段来解决线程安全性问题
    4.Synchronized和Lock锁的区别
    5.什么是AQS、ReentrantLock和ReentrantReadWriteLock锁
    6.JDK自带的线程池有哪几个,线程池的构造方法重要的参数
    7.什么是死锁,怎么避免死锁
    8.CountDownLatch、CyclicBarrier、Semaphore是什么?
    9.Atomic包下的常见子类,什么是CAS,CAS会有什么问题
    10.ThreadLocal是什么?
    .........

    5.线程池

    我这边有个调度系统,运营设置了对应的时间,该任务就去执行,执行的内容大致就是去读HDFS文件,然后将数据组装,再传递到下游。
    任务触发了以后,我们直接将这个任务交给一个线程池去处理,交由线程池后就直接返回SUCCESS。

    这样做的好处是什么?如果多个任务同时触发,那可能某些任务执行时间过长,请求可能会被阻塞住,而我们如果放在线程池中可以提高系统的吞吐量。
    使用线程池的时候,往往我们的调用方都不需要考虑请求是否立马处理成功,假设线程池在处理任务的时候因为某些原因失败了,我们可以走报警机制(用邮件/短信等渠道去提醒请求方即可)。
    不知道大家学过消息队列了没有,我们常常说消息队列是异步的,很多时候调用方的请求我们丢到消息队列里边,就告诉调用方我们这条请求处理成功了,实际上,这个请求可能还交由下游的多个系统去处理,下游的系统可能也是异步的....
    在使用线程池的时候,很多时候我们也是把他当做异步来使(WebFlux实际上也是将请求丢到线程池嘛),只要我们的系统之间交互不是强一致性的,又希望提高系统的吞吐量,我们就可以考虑使用线程池。

    5.1轮询

    有的时候,我们需要有一个线程去轮询处理某些任务。
    比如,我的系统会有发短信的功能,我调用渠道商的下发接口的后,我需要拿到短信的回执信息,于是我就需要去调用渠道商的回执接口。
    此时最简单的做法就是开一个线程,不断的轮询渠道商的回执接口(我们设定轮询的间隔时间即可)

    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        while (true) {
          try {
            // 间隔一段时间轮询一次                                           
            TimeUnit.MILLISECONDS.sleep(period);
            // 调用接口
            String result = http.post();
            // 得到result后进行处理(比如将结果插入到数据库)
            smsDao.insert(result);
          }
        }
      });
    thread.start();
    

    或者有的时候,我们把任务放到内存阻塞队列或者Redis,也是通过一个线程轮询去取「队列」的数据。

    5.2借助juc包实现线程安全

    juc其实就是java.util.concurrent包

    我们在使用线程的时候,或者在日常开发的时候,都是得考虑我们现在使用的场景是否是线程安全的。
    如果不是线程安全的,我们可以做什么东西来使我们的程序变得线程安全。
    1.如果是集合,我们可以考虑一下juc包下的集合类。
    2.如果是数值/对象,我们可以考虑一下atomic包下的类。
    3.如果是涉及到线程的重复利用,我们可以考虑一下是否要用线程池。
    4.如果涉及到对线程的控制(比如一次能使用多少个线程,当前线程触发的条件是否依赖其他线程的结果),我们可以考虑CountDownLatch/Semaphore等等
    5.如果synchronized无法满足你,我们可以考虑lock包下的类

  • 相关阅读:
    wenbao与LCIS(最长公共上升子序列)
    wenbao与cf上升序列(最多改变一个变为连续严格上升序列)
    wenbao与随机
    wenbao与cf(849D)思维
    wenbao与蓝桥
    wenbao与合肥网络赛
    wenbao与HDU
    wenbao与hiho最短路还原
    wenbao与cf连续子序列最大和问题
    wenbao与cf四个人坐车的故事
  • 原文地址:https://www.cnblogs.com/wzdszh/p/14093070.html
Copyright © 2011-2022 走看看