java并发容器
1 并发容器
(1)ConcurrentHashMap 把整个hashmap 分成若干个小的hashmap(segment),每个segment自己加锁(用ReentrantLock),put的时候采用while(trylock()),tryLock是底层是使用cas竞争资源,会一直等待锁资源而不会挂起线程,但是也有次数限制,如果try次数限制大于要求,则会调用lock()函数,lock如果失败则会挂起线程。如果hash的位置是null表明没冲突,直接放入,如果hash冲突则得到head从而得到entry的list放入next。
*计算size,先给3次机会,不lock所有的Segment,遍历所有Segment,累加各个Segment的大小得到整个Map的大小,如果某相邻的两次计算获取的所有Segment的更新的次数(每个Segment都有一个modCount变量,这个变量在Segment中的Entry被修改时会加一,通过这个值可以得到每个Segment的更新操作的次数)是一样的,说明计算过程中没有更新操作,则直接返回这个值。如果这三次不加锁的计算过程中Map的更新次数有变化,则之后的计算先对所有的Segment加锁,再遍历所有Segment计算Map大小,最后再解锁所有Segment。
*rehash,new的时候最后一个参数concurrentLevel代表segment数量,如果指定了数量则segment不能增加,rehash的时候只在自己segment里面增加长度,相对于HashMap的resize,ConcurrentHashMap的rehash原理类似,但是Doug Lea为rehash做了一定的优化,避免让所有的节点都进行复制操作:由于扩容是基于2的幂指来操作,假设扩容前某HashEntry对应到Segment中数组的index为i,数组的容量为capacity,那么扩容后该HashEntry对应到新数组中的index只可能为i或者i+capacity,因此大多数HashEntry节点在扩容前后index可以保持不变。基于此,rehash方法中会定位第一个后续所有节点在扩容后index都保持不变的节点,然后将这个节点之前的所有节点重排即可。
*HashMap,List,Set 都不是线程安全的,可以用Collection.synchronizedMap(new HashMap)这种方法来包装成线程安全的,但只适合并发量比较少的情况,因为被包装以后,各种操作函数都加上了sychronized(){},并发情况下并不能同时操作对象。
(2)BlockingQueue 线程共享队列。可以取数据和加数据,没有数据时,取数据现场会等待,有数据了再唤醒;满数据时,加数据现场会等待,有空位置了再唤醒。性能不是太好但是线程共享数据非常好用。
(3)线程池ThreadPoolExcutor 线程创建和销毁都需要消耗cpu,因此需要线程池来循环使用固定线程,线程池核心是把所有活动线程保留起来。顶层接口Excutor,ExcutorService扩展接口,
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
这几个实现的根本都是实例化了一个ThreadPoolExcutor,通过参数不同确定生成的具体线程池,是个工厂方法。
ThreadPoolExecutor(int corePoolSize,//核心线程池大小(标准数量)
int maximumPoolSize //最多容纳多少线程(扩展最大数量)
long keepAliveTime //空闲时线程存活时间,如果线程数大于corePoolSize,那么多余的线程在超过这个时间以后就会释放掉。
TimeUnit unit, //上面时间的单位
BlockingQueue(Runnable) // 排队等待队列,保存多余的任务 )
所以根据这几个参数,可以得到
* newCachedThreadPool是实例化一个ThreadPoolExecutor(0,Integer.MaxValue,60l,SECOND,SynchronousQueue<Runnable>()),初始线程为0,提交的任务提交到同步队列,但这个队列容量是0,只有线程池拿任务跟程序提交任务同时发生的时候,拿任务和提交任务才会成功,因此这个同步队列只起了个交换数据的桥接作用。线程池会响应提交失败的动作,开始创建线程c处理这个任务,超时时间60秒。一般用于耗时较短的任务,任务处理速度要大于提交速度,不然不断产生新线程内存可能被占满。
* newFixedThreadPool (int nThread) 时间上是实例化一个ThreadPoolExecutor (nThread,nThread,0,SECOND,new LinkedBlockingQueue(Runnable)),是个标准线程和最多线程相等(无法扩容)的拥有无限等待队列的线程池。
* newSingleThreadExecutor其实也是个FixedThreadPool,只是传入的线程数是1.
(4) 拒绝策略 针对上面的构造函数,最后一个参数其实是RejectedExcutionHandler,并发包提供了几个策略,也可以自己继承它实现策略。
ThreadPoolExecutor.AbortPolicy:当线程池中的数量等于最大线程数时抛出java.util.concurrent.RejectedExecutionException异常.
ThreadPoolExecutor.CallerRunsPolicy():当线程池中的数量等于最大线程数时,让调用者自己跑这个任务,不用自己的线程池线程。
ThreadPoolExecutor.DiscardOldestPolicy():当线程池中的数量等于最大线程数时,抛弃线程池中工作队列头部的任务(即等待时间最久Oldest的任务,也就是队列第一个抛弃),并执行新传入的任务
ThreadPoolExecutor.DiscardPolicy():当线程池中的数量等于最大线程数时,丢弃不能执行的新加任务
* 自己可以实现拒绝策略,可以根据个人需求定制行为,比如记录线程信息等。
(5)ForkJoin forkjoin是java7提供的并发框架,Fork把一个大任务拆成若干个小任务,join是汇总各个小任务的结果最终得到大任务的结果。
分割任务:需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
合并结果:分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
*ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
RecursiveAction:用于没有返回结果的任务。
RecursiveTask :用于有返回结果的任务。
*ForkJoinPool :它实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,提高执行效率。 工作窃取算法就是,每个线程都有一个任务队列,这个队列是双向队列,如果一个线程已经处理完自己的队列,可以从另一个队列的另一端拿出任务来执行(同一端可能有并发冲突,加锁会消耗资源)。
2 ThreadLocal
使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。