集合类
Collection接口
Collection的实现主要有List,Set,两者之间的主要区别是,List支持重复,Set不支持,
List的实现包括:ArrayList, LinkedList, Vector,Stacl.;
Set的实现包括:HashSet, TreeSet
Collection的主要方法:
add(E):添加元素
remove(E): 删除
get(index): 得到
contains(E): 含有
iterator(): 得到遍历器
ArrayList
实现方式
创建ArrayList
默认构造器通过调用ArrayList(int)来完成创建,传入值10。代码:
super()调用了默认构造器,是空的。这段代码最重要的就是,创建了一个Object数组,并赋给了当前elementData属性,数组大小是传入的initialCapacity, 因此newArrayList()将会创建一个大小10的数组。
插入对象:add(E)
插入对象时,会根据Object数组当前已有元素属性+1得到一个minCapacity,如果大于
Object数组大小,则先将Object数组赋给另一个数组,然后得到一个新的数组大小=当前数组大小*1.5+1, 如果这个值小于minCapacity,则minCapacity为新数组的大小,然后使用Arrays.copyOf来产生新的数组。
add(E,index)方法:这个在指定位置插入值,首先要确保位置存在且Object数组大小足够,插入后,所有index之后的元素都要向后移一位,这就是多一次复制数组的代价
其他插入方法:addAll(Collection<? extends E>), addAll(int, Collection<?extends E>)
删除对象:remove(E)
判断E是否为null,是则遍历所有元素,null是否存在,存在,则所有其后元素向前复制一位,并将最后的设为null(释放)
E不是null,则通过E的equals来判断对象是否存在。
获取单个对象:get(int)
使用ArrayList最好的地方,get(int)直接得到数组位置的元素(判断int的范围)
遍历对象:iterator()
ArrayList中的内部类Itr,hasNext(),就是当前位置和数组元素大小比较,相等false,否则true
next()方法,会比较创建此Iterator时的modCount属性和当前的modCount属性,如果不相等,说明在遍历时发生了改变Object数组元素的行为(add,remove),抛出ConcurrentModificationException,相等,则取下一个元素(get方法),取不到,抛出IndexOutOfBoundsException
注意:
ArrayList 基于数组实现,无容量限制
增加元素时可能扩大数组,但是删除时不会减小数组大小。对于非null元素,用equals方法判断相等
线程非安全
LinkedList
实现方式
基于双向链表的实现,内部有Entry类,代表元素,有element属性代表value,next属性指向下一个Entry,previous属性指向上一个元素,这种机制有利于快速实现集合中元素的移动
所有的方法基于链表机制,不做解释
非线程安全。
Vector
数组实现。与ArrayList相似
不同处:
add(E)方法在扩容时,如果capacityIncrement属性大于0, 则数组大小为现有size+capacityIncrement,如果capacityIncrement <=0 , 则大小为现有size *2
所以Vector可以通过传入capacityIncrement来控制容量的扩充
add(E),remove(E),get(int)方法都是synchronized的
线程安全的。
Stack
继承与Vector,实现了LIFO,
HashSet
基于HashMap实现
add(E): 通过HashMap的put(Object,Object)方法,将要添加的元素作为key,value传入一个之前已创建的Object
其他方法都是通过HashMap来实现的
非线程安全
TreeSet
基于TreeMap实现,支持排序,非线程安全
Map接口
并发包(java.util.concurrent)
ConcurrentHashMap
线程安全的HashMap实现
ConcurrentHashMap()
和HashMap一样,有initialCapacity属性,loadFactor属性,还多一个concurrencyLevel属性。三个属性默认值是,16,0.75和16
以下方式计算ssize的值
int sshift = 0;
int ssize = 1;
while(ssize <concurrencyLevel){
++sshift;
ssize <<= 1;
}
在concurrencyLevel为16时,ssize为16
ssize参数用于创建Segment对象数组,确定了数组的大小
以下方式计算cap参数
int c =initialCapacity / ssize;
if(c*ssize <initialCapacity)
++ c;
int cap = 1;
while(cap < c)
cap <<= 1;
根据以上参数,cap为1;
cap和loadFactor用于为Segment对象数组创建Segment对象。Segment对象继承ReentrantLock, 在创建时,动作为创建一个大小为cap的HashEntry数组,基于cap和loadFactor计算threshold,threshold = (int)(newTable.length *loadFactor);
put(Objectkey, Object value)
value 不能为null
方法没有加上synchronized
根据传入的key的hashCode, 得到Segment数组中的Segment对象,然后调用Segment的put方法。
对于Segment的put方法,进行lock,完成释放锁。
由此可见ConcurrentHashMap根据concurrencyLevel,分成了对个Segment对象,对key-value进行存储,避免了每次put都锁住整个数组。默认情况下,可有16个线程并发无阻塞的操作对象。
remove(Objectkey)
remove方法同样根据key,找到Segment,调用Segment的remove(Object key)
Segment的remove和put一样lock,然后向HashMap删元素一样操作
get(Objectkey)
get方法一般来说是没有锁的,在并发的情况下加快了数据的获得。
什么情况下加锁,在put方法生成HashEntry对象后,value值还没有设置完成时,并发读取了,就会得到null的value值,这是调用readValueUnderLock方法lock后,返回value
get方法不加锁,怎么保证数据的一致性?
HashEntry数组对象是voliate的,保存在main memory中,一切put和remove的并发操作对于get是马上可见的,当然也有脏数据的可能,比如在get的过程中一次put或remove的完成,会导致不一致,当然这种可能性很低,会了性能是值得的
containsKey(Objectkey)
和get操作一样,没有加锁,步骤也相似
CopyOnWriteArrayList
是一个线程安全的,在读操作时无锁的ArrayList
add(E)
没有使用synchronized关键字,而是使用ReentrantLock来保证线程安全。
和ArrayList不同的是,每次都会创建一个新的Object数组,大小为当前size+1, 将之前的所有元素复制到新的数组中,新的对象加入数组的最后。
remove(E)
和add一样,没有在方法上加synchronized关键字,而是使用ReentrantLock来保证线程安全。
get(int)
为了提高性能,没有加锁
iterator()
在调用iterator之后,创建一个新的COWIterator对象实例,并保存一个当前数组的快照,next对此快照数组进行遍历,所以遍历时不会抛出concurrentModificatiedException。
性能比较
与ArrayList比,随着元素数量和线程的增加,CopyOnWriteArrayList在增加元素和删除元素性能下降很严重,比ArrayList低,但是随着线程增加,查找元素比ArrayList好很多。
CopyOnWriteArraySet
基于CopyOnWriteArrayList实现,不同的是add时,调用了addIfAbsent,确保唯一性。
ArrayBlockingQuene
是个基于数组,FIFO,线程安全的集合类。特色是:可实现指定时间的阻塞读写,并且容量可限制。
ArrayBlockQuene(int)
没有默认构造器,传入的参数即为对象数组的大小,同时初始化锁和两个锁上的Condition,一个为notEmpty, 一个为notFull。
offer(E,long,TimeUnit)
用于插入元素到数组尾部,如数组已满,则等待,直到一下三种情况才继续:被唤醒,到达指定时间,当前线程被中断。
此方法,加锁,判断数组是否满,未满则插入元素,满了,且超过指定时间,返回false。如果未超过指定时间,调用notFullcondition的awaitNanos方法进行等待,如被唤醒或超时,则继续判断数组是否满;如线程被interrupt,则抛出InterruptException
另一个不带时间的offer方法在数组满时,不等待直接返回false。
put方法,在数组满时一直等待,直到数组不为空或线程被中断
poll(E,long TimeUnit)
用于获取队列中的第一个元素,如没有元素,则等待,与offer相同,也在三种情况后继续。
过程:加锁,如果数组元素个数不为0,取最后一个元素,获取后在该位置设null。如数组元素个数为0,则判断等待时间是否大于0, 小于0,则返回null;大于0,调notEmpty condition的awaitNanos方法进行等待…(和offer一样)
另一个不带时间的poll,元素个数为0,直接返回null。
take方法,在数组为空时一直等待,直到数组不为空或线程被interrupt
iterator
先加锁,然后创建一个Itr实例,Itr在创建时获取数组元素尾部的index以及尾部元素,调用完释放锁。
小结
ArrayBlockingQueue为一个基于固定大小数组,ReentrantLock以及Condition实现的可阻塞的FIFO的Queue。
AtomicInteger
是一个支持原子操作的Integer类。实现顺序获取ID的代码
incrementAndGet()
此方法先获取value属性,然后value+1,赋给一个局部的next,明显都是非线程安全的
关键是
if (compareAndSet(current, next))
return next;
compareAndSet调用Sun的unSafe的compareAndSwapInt方法,此方法基于CPU的CAS原语来实现。CAS简单说就是由CPU比较内存位置上的值是否为当前值,如是则将其设置为next,不是返回false。因此上面代码片段是在一个无限循环的代码段中执行,保证并发时ID 的顺序。
基于CAS的操作是可认为无阻塞的,性能好于同步锁的方式。
ThreadPoolExecutor
提供线程池的服务,很容易将一个Runnable接口的任务放入线程池执行。
execute(Runnable)
1如果当前线程数小于配置的corePoolSize, 调用addIfUnderCorePoolSize方法,这个方法:
先调用mainLock加锁,线程池处于RUNNING状态,addThread增加线程,addThread方法先创建Worker对象,调用threadFactory创建线程,如果创建的线程不为null,将Worker对象的thread设置为创建出来的线程,并将此Worker对象放入workers中,最后增加当前线程数,释放mainLock,最后启动新建的线程,完成addIfUnderCorePoolSize的执行。
2.如果当前线程数大于corePoolSize,或addIfUnderCorePoolSize执行失败:
如线程池RUNNING,且往workQueue中成功放入Runnable任务后,则执行后续。
如果线程池不运行或当前运行的线程数0,则调用ensureQueuedTaskHandled方法执行,此方法用于确保workQueue中的command被拒绝或被处理,
如线程池RUNNING或线程池中当前运行线程数不为0,则不做任何动作
3.如不符合以上条件,则调用addIfUnderMaximumPoolSize方法,做法和addIfUnderCorePoolSize方法基本一样,不同点在最大线程数进行比较,超过最大线程数返回false, addIfUnderMaximumPoolSize返回false,则执行reject方法,调用设置的RejectedExecutionHandler的rejectedException方法,ThreadPoolExecutor提供了4种RejectedExecutionHandler的实现:
a)CallerRunsPolicy
当线程池中的线程数=最大线程数后,则交由调用者线程来执行此Runnable任务
b)AbortPolicy
当线程池中的线程数=最大线程数时,抛出RejectedExecutionException
c)DiscardPolicy
当线程池中的线程数=最大线程数时,不做任何动作
d)DiscardOldestPolicy
当线程池中的线程数=最大线程数时,抛弃要执行的最后一个Runnable任务
由以上分析来看JDK提供了要使用ThreadPoolExecutor,要配置corePoolSize,最大线程数,任务缓冲队列,以及线程池满时的处理策略。这些需要根据实际项目需求来决定,常见的需求有如下两种:
-
高性能
高性能的执行Runnable任务,即当线程池中线程数尚未到达最大数,则立即提交给线程执行或在最大线程数量的保护下创建线程来执行。可选方式为使用SynchronousQueue作为任务缓冲队列,SynchronousQueue在进行offer时,如没有其他线程调用poll,则直接返回false,按照ThreadPoolExecutor的实现,此时就会在最大线程数允许下创建线程;
如有其它线程调用poll,则返回true,按照ThreadPoolExecutor execute方法的实现,采用这样的Queue就可实现要执行的任务不会再队列里缓冲,而直接交由线程执行。在这种情况下ThreadPoolExecutor能支持最大线程数的任务数执行。
-
缓冲执行
如希望Runnable任务尽量呗corePoolSize范围内的线程执行,可选方式为使用ArrayBlockingQueue或LinkedBlockingQueue来作为任务缓冲队列。这样,线程数超过corePoolSize后,会先加入缓冲队列,而不是直接交由线程执行。这种情况下,ThreadPoolExecutor最多能支持最大线程数+BlockingQueue大小的任务数执行。
Executors
Executors 提供了一些方法创建ThreadPoolExecutor
newFixedThreadPool(int)
创建固定大小的线程池,线程keepAliveTime为0,默认情况下,ThreadPoolExecutor中启动的corePoolSize数量的线程启动后就一直运行,并不会由于keepAliveTime时间到达后仍没有任务需要执行就退出。缓冲任务队列LinkedBlockingQueue,大小为整型的最大数。传入的task超过了线程池大小后,放入队列,当队列中task数量超过整形的最大值后抛出RejectedExecutionException.
newSingleThreadExecutor()
创建大小为1的线程池,同时只有1个线程执行,其它都在LinkedBlockingQueue队列中
newCachedThreadPool()
创建corePoolSize为0,最大线程数位整型最大值,keepAliveTime为1分钟,缓存队列为SynchronousQueue的线程池。在运行时,放入线程池的task,都会复用线程或启动新线程来执行,当启动的线程数达到整型最大值后抛出RejectedExecutionException,启动后线程存活时间1分钟。
newScheduledThreadPool(int)
corePoolSize为传入参数,最大线程数位整型最大值,keepAliveTime为0,缓存队列DelayedWorkQueue, 在需要延时或定时执行的业务,如异步超时回调等,使用这个Executor。JDK5之前,用Timer实现这个功能,两者区别:
-
Timer单线程,一旦一个task运行缓慢,其它的task也会推迟,ScheduledThreadPool并发
-
Timer中的task抛出RunntimeException时,其它task也不会执行
-
ScheduledThreadPool可以执行callable的task
Semaphore
用于控制某资源被访问的个数的类。
比如控制连接池连接各数的传统方法:
而改用Semaphore后,不那么复杂了。
在tryAcquire方法中,只有有可用的资源时,返回true,当可用资源小于0时,等待,知道超时返回false。
CountDownLatch
可用于控制多个线程同时开始某动作的类,采用的方式为减计数方式。
CyclicBarrier
用于当await的数量达到了设定的数量后,才继续往下执行。
ReentrantLock
更为方便的控制并发资源的类。基于AbstractQueuedSynchronizer的实现,AbstractQueuedSynchronizer为基于整数状态值实现资源的并发控制访问提供了很好的支持。
Condition
接口,典型实现ReentrantLock,ReentrantLock提供了一个newCondition方法,以便用户在同一个锁的情况下可以根据不同的情况执行等待或唤醒动作。
ReentrantLock.newCondition().await()
ReentrantLock..newCondition().signal()
ReentrantReadWriteLock
和ReentrantLock没有继承关系,它提供了读锁(ReadLock)和写锁(WriteLock),比ReentrantLock的一把锁,读写锁分离对读多写少的情况,提高性能。
当调用读锁的lock方法时,没有线程持有写锁,就可获得读锁,意味着只要进行读操作,就没有其它线程进行写操作,读操作时无阻塞的;
当调用写锁的lock时,如果此时没有线程持有读锁或写锁,则可继续,意味着要进行写时,如果有其它线程进行读或写,就会被阻塞,
读写锁使用时的升级和降级机制:
同一线程中,持有读锁后,不能直接调用写锁的lock
同一线程中,持有写锁后,可调用读锁的lock方法,之后如果调用写锁的unlock方法,那么当前锁将降级为读锁。