并发与多线程(三) 并发容器和线程池

并发容器

常用的并发容器:

ConcurrentHashMap

万字图文——ConcurrentHashMap源码深度解析   

        我们平时最常用的HashMap其实不是线程安全的,多线程使用场景下,即想线程安全,又想拥有Map的能力,我们可以选择 HashTable ,因为它是针对我们常用的方法上面加上了 synchronized 锁,但是在高并发的场景下,效率低是它的弊端。如果我们还非常在意效率,那么我们更好的选择是使用ConcurrentHashMap

        JDK1.8之前,ConcurrentHashMap采用分段锁(Segment + ReentrantLock)保证线程安全.它将内部的table数组分为了多个段(Segment<K,V> extends ReentrantLock),默认16(DEFAULT_CONCURRENCY_LEVEL = 16),也就是最大并发量,相当于每一个Segment都有自己的一把锁, 细化了锁的粒度,降低了锁竞争的频率.

        而在JDK1.8, 又把分段锁修改为了CAS+synchronized, 再次细化了锁粒度, 对数组位置上的头结点进行加锁, 也就是数组中的每个元素都可以作为一个锁。在对应数组位置上没有值的情况下,直接通过 CAS 操作来插入; 如果当前位置已经存在值的话,那么就使用 synchronized 关键字对链表头结点加锁,再进行之后的 hash 冲突处理。

ConcurrentLinkedQueue

        ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,在高并发环境中性能很好, 底层由单向链表组成, 每个节点包含了当前节点的值以及下个节点引用,采用先进先出规则.

CopyOnWriteArrayList

        在大多数的应用场景中,读操作的比例远远大于写操作。那么,当执行读操作的时候,对数据是没有修改的,所以,无须对数据进行加锁操作。而针对于写操作的场景中,则需要加锁来保证数据的正确性。CopyOnWriteArrayList就可以满足上面所说的场景: 读操作不加锁,而写操作也不会阻塞读的操作写入操作时,进行一次自我复制产生一个副本,写操作就在副本中执行,写完之后,再将副本替换原来的数据。

BlockingQueue 阻塞队列

        BlockingQueue是一个接口,只要实现了这个接口的所有实现类,都可以作为阻塞队列而应用在线程池中,是线程池ThreadPoolExecutor中的核心参数之一.

BlockingQueue常用方法:

BlockingQueue的实现类

 ArrayBlockingQueue

        默认采用非公平锁, 比较核心的两个属性notEmpty和notFull

构造函数

 入队操作 put() 和 enqueue()

 出队操作 take() 和dequeue()

LinkedBlockingQueue

构造函数

出队 take()和dequeue()

SynchronousQueue

阻塞队列——SynchronousQueue源码解析-缪斯

        SynchronousQueue是通过CAS实现,是线程安全的. 和其他队列不同的是SynchronousQueue的capacity=0,即SynchronousQueue不存储任何元素。SynchronousQueue的每一次insert操作,必须等待其他线程的remove操作, 而每一个remove操作也必须等待其他线程的insert操作, 可以认为这是一种线程与线程间一对一传递消息的模型

线程池 ThreadPoolExecutor

        线程池是一种针对线程创建和回收的池化技术,类似连接池.线程是程序非常核心且珍贵的资源,线程池则是为了更为方便和安全的帮我们创建和管理线程, 避免线程频繁的创建和销毁带来的性能消耗和安全问题.

线程池的工作原理大致为4步:

● 首先,当有任务要执行的时候,会计算线程池中存在的线程数量与核心线程数量(corePoolSize)进行比较,如果小于,则在线程池中创建线程,否则,进行下一步判断。

● 其次,如果不满足上面的条件,则会将任务添加到阻塞队列(1.6 阻塞队列)中。等待线程池中的线程空闲下来后,获取队列中的任务进行执行。

● 第三,如果队列中也塞满了任务,那么会计算线程池中存在的线程数量与最大线程数量(maxnumPoolSize)进行比较,如果小于,则在线程池中创建线程。

● 最后,如果上面都不满足,则会执行对应的拒绝策略

核心参数
public ThreadPoolExecutor(int corePoolSize,//核心线程数
                              int maximumPoolSize,//最大线程数
                              long keepAliveTime,//空闲存活时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              ThreadFactory threadFactory,//线程工厂
                              RejectedExecutionHandler handler//拒绝策略
) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

//    参数如何来设置
//        * 需要根据几个值来决定
//        - tasks :每秒的任务数,假设为1000
//        - taskcost:每个任务花费时间,假设为0.1s
//        - responsetime:系统允许容忍的最大响应时间,假设为1s
//        * 做几个计算
//        - corePoolSize = 每秒需要多少个线程处理?
//        * 一颗CPU核心同一时刻只能执行一个线程,然后操作系统切换上下文,核心开始执行另一个线程的代码,以此类推,超过cpu核心数,就会放入队列,如果队列也满了,就另起一个新的线程执行,所有推荐:corePoolSize = ((cpu核心数 * 2) + 有效磁盘数),java可以使用Runtime.getRuntime().availableProcessors()获取cpu核心数
//            - queueCapacity = (coreSizePool/taskcost)*responsetime
//            * 计算可得 queueCapacity = corePoolSize/0.1*1。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
//            * 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
//        - maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
//        * 计算可得 maxPoolSize = (1000-corePoolSize)/10,即(每秒并发数-corePoolSize大小) / 10
//        * (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
//            - rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
//            - keepAliveTime和allowCoreThreadTimeout采用默认通常能满足

// 原文链接:https://blog.csdn.net/mythsmyths/article/details/131961323
常用几种线程池类型

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadScheduledExecutor

创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程会代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与newScheduledThreadPool不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ForkJoinPool

创建子线程去执行某些任务,最后等待所有子线程的结果进行汇总.

ThreadPoolTaskExecutor

spring中对ThreadPoolExecutor进行封装的线程池

拒绝策略

        当核心线程满时, 新的任务会进入到阻塞队列; 阻塞队列满时, 线程池会创建新的线程; 当线程池的线程数达到最大线程数时,需要执行拒绝策略:

  • AbortPolicy(默认) 抛出异常,中止任务。抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行.
  • CallerRunsPolicy 使用调用线程执行任务。当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
  • DiscardPolicy - 直接丢弃当前新进来的任务
  • DiscardOldestPolicy - 丢弃队列最老任务,添加新任务。当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

Future

        Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果, 必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。我们可以通过它来实现多线程各自分别做任务的一部分,最后当所有子线程都执行完毕后,再将子结果进行组织或封装。

CompletableFuture

CompletableFuture使用详解

        CompletableFuture是对Future的扩展和增强。CompletableFuture实现了Future接口,并在此基础上进行了丰富的扩展,完美弥补了Future的局限性,同时CompletableFuture实现了对任务编排的能力。借助这项能力,可以轻松地组织不同任务的运行顺序、规则以及方式.

  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值