博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
线程池高度概括
阅读量:4307 次
发布时间:2019-06-06

本文共 8658 字,大约阅读时间需要 28 分钟。

  • 关于线程和线程池,我们必须知道以下几个概念:
    • 第一,线程中的基本概念,
    • 第二,线程的生命周期
    • 第三,单线程和多线程
    • 第四,什么是多线程的安全问题?为什么会造成多线程的安全问题呢?
    • 第五,线程池的原理解析
    • 第六,常见的几种线程池的特点以及各自的应用场景
  • 一、线程,程序执行流的最小执行单位,是进程中的实际运作单位,经常容易和进程这个概念混淆。
    • 那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。
  • 二、线程的生命周期
    • 线程的生命周期可以利用以下的图解来更好的理解:
        • 第一步,是用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。
        • 而我们在图解中可以看出,线程还具有一个阻塞的过程,这是怎么回事呢?当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程
  • 三、什么是单线程和多线程?
    • 单线程,顾名思义即是只有一条线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解
    • 多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。
    • 并行和并发:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢?
      • 什么是并发?
        • 并发:指应用能够交替执行不同的任务,例:吃饭的过程:吃米饭->吃菜 ->........->吃完最后的米饭
      • 什么是并行?
        • 并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
      • 两者区别:一个是交替执行,一个是同时执行.
  • 四,还有就是多线程的安全问题?为什么会造成多线程的安全问题呢?
    • 我们可以想象一下,如果多个线程同时执行一个任务,那么意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,那么,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源。
    • 由造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程占据CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。
    • 什么是线程安全性?
    • 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
    • 正确性:某个类的行为与其规范相一致。(我理解的规范就是我们在编写类时,能预知的状态结果)
    • 原子性
      • 竞态条件(Race Condition):某个计算的正确性取决于多个线程的交替执行的时序。(线程的时序不同,产生的结果可能会不同)
      • “先检查后执行”,即通过一个可能失效的观测结果来决定下一步的操作。
      • 首先观察到某个条件为真,然后开始执行相关的程序,但是在多线程的运行环境中,条件判断的结果以及开始执行程序中间,观察结果可能变得无效(另外一个线程在此期间执行了相关的动作),从而导致无效。常见的就是(Lazy Singleton)
      • “读取-修改-写入”,基于对象之前的状态来定义对象状态的转换。即使是volatile修饰的变量,在多线程的环境里面进行自增操作,同样会发生竞态条件,所以volatile不能保证绝对的线程安全(360面试问题)。
      • 引用书中定义:假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
    • 加锁机制
      • 在线程安全的定义中,多个线程间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。当不变性条件中涉及多个变量时,各个变量之间并不是互相独立的,一个变量发生变化会对其他变量的值产生约束。因此,一个变量发生改变,在同一个原子操作里面,其他相关变量也要更新。
      • 内置锁:同步代码块(Synchronized Block)包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。关键字Synchronized修饰方法就是一种同步代码块,锁就是方法调用所在的对象,静态的Synchronized方法以Class对象作为锁。内置锁或监视锁就是以对象作为实现同步的锁。
      • Java内置锁,进入的唯一途径是执行进入由锁保护的同步代码块或方法。它相当于一种互斥锁。
      • 重入锁:当一个持有锁的线程再次请求进入自己持有的锁时,该请求会成功。"重入"意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方式,为每个锁关联一个计数器和线程持有者。
    • 用锁来保护状态
      • 由于锁能使其保护的代码路径以串行形式访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
      • 对象的内置锁与其状态之间没有内在的联系,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。
      • 对于每个包含多个变量的不变性条件,其中涉及的所有变量都要使用同一个锁来保护/同步。
    • 解决方案
      • 多线程并发不安全的原因已经知道,那么针对这个种情况,java中有两种解决思路:
        • 给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。
        • 让线程也拥有资源,不用去共享进程中的资源。
      • 基于上面的两种思路,下面便是3种实施方案:
      • 1. 多实例、或者是多副本(ThreadLocal):对应着思路2,ThreadLocal可以为每个线程的维护一个私有的本地变量,可参考java线程副本–ThreadLocal;
      • 2. 使用锁机制 synchronize、lock方式:为资源加锁
      • 3. 使用 java.util.concurrent 下面的类库:有JDK提供的线程安全的集合类
  • 五,线程池原理解析
    • 又以上介绍我们可以看出,在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
    • 线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
      • 那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor
        • 而我们创建时,一般使用它的子类:ThreadPoolExecutor . #(线程池执行器)
        • public ThreadPoolExecutor(
        • int corePoolSize, # 线程池中的核心线程数量
        • int maximumPoolSize, # 线程池中可以容纳的最大线程的数量
        • long keepAliveTime, # 就是线程池中除了核心线程之外的其他的最长可以保留的时间
        • TimeUnit unit, # 计算这个时间的一个单位
        • BlockingQueue workQueue, # 就是等待队列,任务可以储存在任务队列中等待被执行
        • ThreadFactory threadFactory, # 就是创建线程的线程工厂,执行的是FIFIO原则(先进先出)
        • RejectedExecutionHandler handler) # 是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
    • 这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:
        • 从图中,我们可以看出,线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收,maximumPoolSize就是线程池中可以容纳的最大线程的数量,而keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,而util,就是计算这个时间的一个单位,workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
          • 线程池的执行流程又是怎样的呢?
              • 由图我们可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
                • handler的拒绝策略:
                  • 有四种:第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
                  • 第二种DisCardPolicy:不执行新任务,也不抛出异常
                  • 第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
                  • 第四种CallerRunsPolicy:直接调用execute来执行当前任务
              • 线程池任务执行流程:
                • 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
                • 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
                • 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
                • 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
                • 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
                • 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
  • 六,四种常见的线程池
    • 如果你不想自己写一个线程池,Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的。
    • newCachedThreadPool() 没有核心线程的,无限个
      • 可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大
      • 底层:返回ThreadPoolExecutor实例,
        • corePoolSize为0;
        • maximumPoolSize为Integer.MAX_VALUE;
        • keepAliveTime为60L;
        • unit为TimeUnit.SECONDS;
        • workQueue为SynchronousQueue(同步队列)
      • 通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
      • 适用:执行很多短期异步的小程序或者负载较轻的服务器
      • 创建方法:
      • ExecutorService mCachedThreadPool = Executors.newCachedThreadPool();
      • 用法:
        • //开始下载
        • private void startDownload(final ProgressBar progressBar, final int i) {
        • mCachedThreadPool.execute(new Runnable() {
        • @Override
        • public void run() {
        • int p = 0;
        • progressBar.setMax(10);//每个下载任务10秒
        • while (p < 10) {
        • p++;
        • progressBar.setProgress(p);
        • Bundle bundle = new Bundle();
        • Message message = new Message();
        • bundle.putInt("p", p);
        • //把当前线程的名字用handler让textview显示出来
        • bundle.putString("ThreadName", Thread.currentThread().getName());
        • message.what = i;
        • message.setData(bundle);
        • mHandler.sendMessage(message);
        • try {
        • Thread.sleep(1000);
        • } catch (InterruptedException e) {
        • e.printStackTrace();
        • }
        • }
        • }
        • });
        • }
    • newSecudleThreadPool() 周期性的
      • 周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
      • 底层:创建ScheduledThreadPoolExecutor实例,
        • corePoolSize为传递来的参数,
        • maximumPoolSize为Integer.MAX_VALUE;
        • keepAliveTime为0;(保留时间)
        • unit为:TimeUnit.NANOSECONDS;(时间单位)
        • workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列
      • 通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
      • 适用:周期性执行任务的场景
      • 创建方法:
        • //nThreads => 最大线程数即maximumPoolSize
        • ExecutorService mScheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
      • 用法:
        • private void startDownload(final ProgressBar progressBar, final int i) {
        • mFixedThreadPool.execute(new Runnable() {
        • @Override
        • public void run() {
        • //....逻辑代码自己控制
        • }
        • });
        • }
    • newSingleThreadPool() 只有一个线程的
      • 底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,
        • corePoolSize为1;
        • maximumPoolSize为1;
        • keepAliveTime为0L;
        • unit为:TimeUnit.MILLISECONDS;
        • workQueue为:new LinkedBlockingQueue<Runnable>() 无界阻塞队列
      • 通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
      • 适用:一个任务一个任务执行的场景
      • 创建方法:
        • ExecutorService mSingleThreadPool = Executors.newSingleThreadPool();
      • 用法:
        • private void startDownload(final ProgressBar progressBar, final int i) {
        • mFixedThreadPool.execute(new Runnable() {
        • @Override
        • public void run() {
        • //....逻辑代码自己控制
        • }
        • });
        • }
    • newFixedThreadPool() 没有核心线程的,有限个
      • 定长的线程池,核心线程的即为最大的线程数量,没有非核心线程。所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。
      • 如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。
      • 底层:返回ThreadPoolExecutor实例,
        • 接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;
        • keepAliveTime为0L(不限时);
        • unit为:TimeUnit.MILLISECONDS;
        • WorkQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
      • 通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
      • 适用:执行长期的任务,性能好很多
      • 创建方法:
        • //nThreads => 最大线程数即maximumPoolSize
        • ExecutorService mFixedThreadPool= Executors.newFixedThreadPool(int nThreads);
      • 用法:
        • private void startDownload(final ProgressBar progressBar, final int i) {
        • mFixedThreadPool.execute(new Runnable() {
        • @Override
        • public void run() {
        • //....逻辑代码自己控制
        • }
        • });
        • }
    • 备注:
      • 一般如果线程池任务队列采用LinkedBlockingQueue(无界)队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小被忽略了。
      • 如果线程池任务队列采用ArrayBlockingQueue队列的话,那么ThreadPoolExecutor将会采取一个非常负责的算法,比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue(等待队列)最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。
      • 这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀—最大线程数就起作用了。

转载于:https://www.cnblogs.com/StarsBoy/p/10039338.html

你可能感兴趣的文章
设计模式07_建造者
查看>>
设计模式08_适配器
查看>>
设计模式09_代理模式
查看>>
设计模式10_桥接
查看>>
设计模式11_装饰器
查看>>
设计模式12_外观模式
查看>>
设计模式13_享元模式
查看>>
设计模式14_组合结构
查看>>
设计模式15_模板
查看>>
海龟交易法则01_玩风险的交易者
查看>>
CTA策略02_boll
查看>>
vnpy通过jqdatasdk初始化实时数据及历史数据下载
查看>>
设计模式19_状态
查看>>
设计模式20_观察者
查看>>
vnpy学习10_常见坑02
查看>>
用时三个月,终于把所有的Python库全部整理了!拿去别客气!
查看>>
pd.stats.ols.MovingOLS以及替代
查看>>
vnpy学习11_增加测试评估指标
查看>>
资金流入流出计算方法
查看>>
海龟交易法则07_如何衡量风险
查看>>