并发容器
常用的并发容器:
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是通过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.youkuaiyun.com/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是对Future的扩展和增强。CompletableFuture实现了Future接口,并在此基础上进行了丰富的扩展,完美弥补了Future的局限性,同时CompletableFuture实现了对任务编排的能力。借助这项能力,可以轻松地组织不同任务的运行顺序、规则以及方式.