线程池概述

线程池

什么是线程池

我们通常可以new Thread(()->{线程执行的任务}).start()这种形式开启一个线程,当run()方法运行结束后,线程对象会被gc释放

在真实的环境中,可能需要很多线程来支撑整个应用,当线程数量非常多的时候,反而会耗尽cpu的资源,如果不对线程进行控制与管理,反而会影响程序的性能,线程开销主要包括:创建启动线程的开销;线程销毁的开销;线程调度的开销,即上下文切换时的开销,而且线程的数量会受限cpu处理器的数量的影响

线程池则是有效使用线程的一种常用方式,线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池,线程池将这些任务缓存在工作队列中,线程池中的工作线程不断的从队列中取出任务并执行
在这里插入图片描述

核心线程池的底层实现

查看executors工具类中newCachedThreadPool()、new SingleThreadExecutor()、newFixedThreadPool()源码

newCachedThreadPool()

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

使用的直接提交的任务队列,这个队列不会实际的存储任务,而是在每一次提交任务时创建一个线程,适合执行大量耗时短并且提交频繁的任务

new SingleThreadExecutor()

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

在任意时刻只有一个线程在执行任务,消费者生产者模式中就可以使用这个线程池

newFixedThreadPool()

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

不难看出这些返回线程池的方法都使用了ThreadPoolExecutor线程池,这些方法都是ThreadPoolExecutor线程池的封装

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

其构造方法中参数的含义为

  • corepoolsize: 指定线程池中核心线程的数量
  • maxinumpoolsize:指定线程池中最大线程数量
  • keepalivetime:当线程池中的数量,超过corepoolsize时,多于的空闲线程的存活时长,即空闲时长在多长时间内销毁
  • unit:时间的枚举类,keepalivetime时长单位
  • workqueue:任务队列,把任务提交到该任务队列中等待执行
  • threadfactory:线程工厂,用于创建线程
  • handler:拒绝策略,当任务太多来不及处理时,如何拒绝

说明:

workqueue工作队列是指提交未执行的任务队列,它是Blockqueue接口的对象,仅用于存储runnable任务,根据队列功能分类,在ThreadPoolExecutor构造方法中可以使用以下几种阻塞队列

  1. 直接提交任务队列,由synchronousQueue对象提供,该队列没有容量,提交给线程池的任务不会真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,如果线程数量已经达到最大数量,则执行拒绝策略
  2. 有界任务队列,由ArraryBlockinQueue实现,在创建ArraryBlockinQueue对象时,可以指定一个容量,当有任务需要执行时,如果线程池中线程数量小于核心线程数量,则会创建新的线程,如果已经达到核心线程数量的大小,则加入等待队列,如果队列已经满了已经无法加入,在线程小于最大线程数量的前提下,会创建新的线程来执行,如果线程大于最大线程数量,则会执行拒绝策略
  3. 无界队伍队列,由LinkingBlockinQueue对象实现,与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况,当有新任务时,在系统线程数小于核心线程数则创建新的线程来执行任务,当线程池中线程数量大于corepoolsize核心线程数则把任务加入阻塞队列
  4. 优先任务队列,PriorityBlockingQueue实现的,是带有任务优先级的队列,是一个特殊的无界队列,不管是ArraryBlockinQueue,还是LinkingBlockinQueue都是按照队列的先进先出的方式处理任务的,而在PriorityBlockingQueue可以根据任务优先级顺序先后执行
拒绝策略

当提交给线程池的任务数量超过了实际承载能力时,如何处理?及线程池中的线程已经用完了,等待队列也满了,无法为新的任务服务就可以通过拒绝策略来处理这个问题,jdk内置了四种拒绝策略:

  1. abortpolicy策略,会抛出异常
  2. callerrunspolicy策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的线程
  3. discardoldestpolicy策略将任务队列中最老的任务丢弃,尝试再次提交新任务
  4. DisardPolicy策略直接丢弃这个无法处理的任务

executors工具类提供的静态方法返回的线程池的默认拒绝策略是abortpolicy即排除异常

如果内置的策略无法满足,还可以自定义,即扩展RejectedExecutionHandler接口

代码如下

package com.cxf.test;

import java.util.concurrent.*;

public class test1 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1),
                Executors.defaultThreadFactory(), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("满了,俺feifei处理不了");
            }
        }
        );
        for (int i = 0; i <3 ; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
threadfactory

线程池中的线程从哪来的?

答案是threadfactory

threadfactory是一个接口,只有一个用来创建线程的方法

同样的该方法我们也

优化线程池的数量

线程池对系统性能是有一定影响的,过大或者过小都会无法发挥最优的系统性能,线程池大小不需要非常精确,只要避免极大或者极小的情况即可,一般来说,线程池的大小需要考虑cpu数量,内存大小等因素可以来自定义,这里就不再演示
在这里插入图片描述

线程池死锁

如果在线程池中执行的任务a在执行过程中又向线程池中提交了任务b,任务b添加到了线程池的等待队列中,如果任务a的结束需要等待任务b的执行结果就有可能会出现这种情况,线程池中的所有工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待执行,线程池中没有可以对阻塞队列中的任务进行处理的线程,这种等待就会一直持续下去,从而造成死锁

适合给线程池提交相互独立的任务,而不是相互依赖的任务,对于相互依赖的任务,可以考虑提交给不同的线程池

保障线程安全的设计技术

从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以使我们在不必借助锁的情况下保障线程安全,避免锁可能导致的问题及开销

java运行时存储空间

java运行时空间可以分为栈区,堆区与方法区

栈空间为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时,就为线程分配栈空间,在线程中每调用一个方法就给方法分配一个栈帧,栈帧里的局部变量表用来存储局部变量,返回值等私有数据,基本变量存储在栈空间中,引用类型的变量值也是存储在栈空间中,引用的对象存储在堆中,由于线程间是相互独立的,一个线程不能访问另一个线程的栈空间,因此线程对局部变量以及只有通过当前线程的局部变量才能访问的对象进行的操作都是线程安全的

而堆中存储的对象,是多个线程可以共享的,如果此时多线程并发操作,共享对象,且有数据的修改就有可能出现线程不安全的情况

非堆空间,即jdk8后,称为元空间,存储类元数据,方法信息,jit代码缓存等也是多个线程共享的,因此也有可能出现线程不安全现象

无状态对象

对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态,实例变量与静态变量称为状态变量

如果一个类的同一个实例被多个线程共享,不会使这些线程存在共享状态,那么就称这个类为无状态对象,反之,则称为有状态对象,实际上无状态对象就是不包含任何实例变量,也不包含任何静态变量的对象

线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种方法就是避免在多个线程之间共享数据,使用无状态对象就是这种方法

不可变对象

不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性,当不可变对象现实实体发生改变时会创建一个新的不可变对象,就如string字符串对象,一个不可变对象需要满足以下条件:

  1. 类本身使用final修饰,防止其子类改变其定义
  2. 所有的字段都是final修饰的,final字段在创建对象时必须显示初始化,不能被修改
  3. 如果字段引用了其他状态可变的对象(集合数组),则这些字段必须是private私有的

不可变对象的应用场景:

1.被建模对象的状态变化不频繁

  1. 同时对一组相关对象进行写操作,可以应用不可变对象,既可以保障原子性也可以避免锁的使用
  2. 使用不可变对象作为安全可靠的map建,hashmap键值对的存储位置,与key的hashcode有关,如果键的内部状态发生了变化会导致hashcode的不同,可能会影响键值对的位置,如果key是不可变对象,则hashcode方法的返回值恒定,位置就是固定的
线程特有对象

我们可以不共享非线程安全的对象,对于非线程安全的对象,每个线程都创建一个该对象的实例,各个线程访问各自创建的实例,一个线程不能访问另一个线程创建的实例,这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象,线程特有对象即保障了对非线程安全对象的访问的线程安全,又避免了锁的开销,线程特有对象也具有固有的线程安全性

threadloacl 类相当于线程访问其特有对象的代理,即各个线程通过threadlocal对象可以创建并访问各自的线程特有对象,一个线程可以使用不同的threadloacl实例来创建访问不同的线程特有对象

threalocal实例为每个访问它的对象都关联了一个该线程特有的对象,threadloacl实例都有当前线程与特有实例之间的一个关联

装饰器模式

装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象,外包装对象与非线程安全的对象有相同的接口,即外包装对象的使用方式与线程安全的对象的使用方式完全相同,而外包装对象通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法

在java.util.collections工具类中提供了一组synchronizedxxx(xxx)可以把不是线程安全的xxx集合转换为线程安全的集合,他就是采用了这种装饰器模式,这个方法的返回值就是指定集合的外包装对象,这类集合又被称为同步集合

使用装饰器模式的好处就是实现关注点分离,在这种设计模式中,实现同一组功能的两个版本;非线程安全的对象和线程安全的对象,对于非线程安全的在设计时只需要关注实现的功能,线程安全的只需要关注线程的安全性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Quare_feifei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值