Java并发包中的数据结构-线程池

Java并发包中的数据结构-线程池

2020-04-15    09'14''

主播: 橙汁儿橙汁儿。

156 3

介绍:
1.并发队列 (1)ConcurrentLinkedQueue:单链表,通过 CAS 操作,未加锁,size 不准确。 (2)ArrayBlockingQueue:有界数组,通过 ReentrantLock 加锁,加锁粒度大,入队、出队只允许一个。 offer、pull 没有用到条件变量,put、take 用到了条件变量。 (3)LinkedBlockingQueue:有界单链表,默认长度是 2 的 31次方-1,可以指定。 通过两个 ReentrantLock 加锁,入队、出队可以同时进行, put()、take() 会用到两个条件队列。 (4)PrioryBlockingQueue:无界队列,内部有个优先队列,ReentrantLock 加锁,CAS 扩容。 (5)DelayBlockingQueue:ReentrantLock 加锁,每次出队的是要过期的元素。 2.ConcurrentHashMap HashMap 是线程不安全的,Hashtable 用了 synchronized 加锁,虽然线程安全,但是性能不高。 (顺便说一下 Hashtable 默认长度是 11,扩容方式是 *2+1,散列方式是取哈希码,而不对数组长度取余。) 高并发应该用 ConcurrentHashMap 。 JDK1.8 以前,是分段锁,内部是 HashEntry 数组,volatile 保证内存可见性,默认情况下,Segments 数组中有 16 个 Segment,扩容也是对 Segment 进行扩容。Segment 继承了 ReentrantLock,每个 Segment 中有若干个 HashEntry,读时可以多个线程读,而写时,每个Segment 中最多只能有一个线程写,也就是说最多有 16 个线程在 各自的 Segment 中写。 而 JDK 1.8以后,取消了 Segment,而是采用 Node数组 + CAS + synchronized ,数组每个元素链着链表,当链表长度达到 8 会转化成红黑树,当数组元素为 null 时,使用 CAS 更新,如果不为 null ,使用 synchronized 保证线程安全,去连链表或转化成红黑树。 JDK 1.6之后对 synchronized 进行了优化,比如 轻量级锁、偏向锁,性能不比 ReentrantLock 差。 3.CopyOnWriteArrayList 写时复制,使用 ReetrantLock 加锁,在增、删、改数组时,是拷贝一个数组进行操作的,再将数组引用指向新数组。 存在弱一致性问题,比如读取数组元素时,是先获取数组,再访问下标,如果获取数组后,有其他线程修改了数组,再访问下标时访问的还是原来数组的下标,就出现了不一致的问题。 4.CopyOnWriteArraySet 底层有个 CopyOnWriteArrayList,每次增加元素时,用 ReentrantLock 加锁,会遍历 CopyOnWriteArrayList,如果有重复的返回 false,如果遍历完毕没有重复的才会把新元素放在数组末尾。 5.线程池使用场景 适用于异步、并发执行任务。 (1)可以避免因为线程频繁的创建和销毁带来性能的损耗; (2)而且响应速度较快; (3)线程池可以对线程进行统一的管理、分配。 6.创建线程池用到的参数 核心线程个数 corePoolSize 最大线程个数 MaxiumPoolSize 工作队列 BlockingQueue 最长存活时间 keepAliveTime(如果线程个数大于核心线程个数,而且是闲置状态,最多经过 keepAliveTime 会被回收,也就是说 除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的。) 时间对应的单位 timeUnit 拒绝策略 默认是(1)抛出异常,还有:(2)使用调用者所在线程执行任务 (3)丢弃队列最近的任务,执行当前任务 (4)抛弃掉,不处理 7.将任务提交到线程池的过程: execute(Runnable task)是没有返回值的,submit 会返回一个 futuretask。 首先会看当前线程个数是否小于核心线程池个数,如果小于的话就会新建一个线程执行任务。如果不小于,会先检查线程池状态,处于 RUNNING 才可以执行任务,会把线程加到工作队列中,如果队列已满,就会新建一个线程执行任务,如果新建失败,说明已达到最大线程个数,会使用拒绝策略; 把线程加到工作队列之后,会再进行一次 线程池状态检查,如果不处于 RUNNING 状态,会从工作队列中删除线程,并使用拒绝策略,否则会看线程池是否为空,为空的话会创建线程。 总的来说,是先看核心线程池是否满,未满的话就新建线程执行任务,否则去看阻塞队列是否已满,未满的话就新建线程执行任务,否则就看是否达到线程池最大容量,未达到就新建线程执行任务,否则就是要拒绝策略。 创建线程是把线程封装成 worker 线程,每次 worker 执行完任务都会循环获取工作队列中的任务执行。 8.关闭线程池 总的来说,是遍历线程池中的工作线程,逐个调用 interrupt() 方法。 shutDown()是将线程池状态设置为 SHUTDOWN,正在执行的线程会继续执行,而未被执行的线程:空闲线程会被中断,返回 void。 shutDownNow()是将线程池状态设置为 STOP,正在执行的线程被中断,立刻返回未被执行的线程列表。 9.线程的配置 CPU 也就是运算密集的,建议线程数设置为 CPU+1,额外的 1 个线程用于处理 CPU 异常,避免CPU 周期中断。 I/O 密集的,应该充分利用 CPU,建议线程数设置为 CPU数*(1+IO耗时/CPU耗时),如果都是I/O耗时,那么线程数就是 CPU数*2。 10.常见线程池 (1)FixedThreadPool:以 LinkedBlockingQueue 作为工作队列,默认长度是 2 的 31 次方-1,所以适用于 服务负载较重的情况,因为线程可重用,不会使用拒绝策略。 (2)SingleThreadPool:以 长度为 1 的LinkedBlockingQueue 作为工作队列,适用于需要按顺序、任何时刻只能有一个线程的情况。 (3)SchealPoolExecutor:自线程开启的指定纳秒后执行任务,工作队列是 DelayBlockingQueue。 (4)CachedThreadPool:工作队列是 SynchronizedQueue,无容量限制,适用于 短期、异步任务,负载低的,如果提交任务的速度快于线程池处理任务的速度,就会一直创建线程。