一、容器
· 在进行安全性容器的认识之前,先了解一下java中常见的几种容器。java中的容器在物理上可以分为俩种:Collection和Map。区别就是在存储时按对存储还是单值存储。以下是画的简略图:
二、并发容器
主要是为之后的线程池打基础。
我们常用的一些容器例如 ArrayList、HashMap、都不是线程安全的,在高并发情况下,会出现线程安全问题,此时就需要将这些容器变为线程安全的容器,最简单的方法是给这些容器所有的方法都加上 synchronized 关键字。这也是java最早期的并发容器以及Collections中各种容器的实现原理:
容器发展历程: 1:Vector Hashtable :早期使用,由synchronized实现,知道就行,基本不用 2:ArrayList HashSet :此时考虑效率出现非同步容器 3:Collections.synchronized***:此时在Map或者List基础上实现了同步,工厂方法使用的也是synchronized,其实和HashTable区别就是,锁的粒度变小了 4:ConcurrentHashMap等:进行性能优化 5:这个不是说后边一定比前边好,要灵活应用
1、Map型同步容器
1)ConcurrentHashMap
对于Map型来说平时用的最多的就是ConcurrentHashMap,也不是说他一定比HashTable和Collection.synchronizedMap好,其实你如果拿测试案例试一下,在写并发特别大的时候HashTable和Collection.synchronizedMap比ConcurrentHashMap效率更高,因为ConcurrentHashMap加锁方式相对来说比较复杂,加的是桶锁,而HashTable使用的是Hash表锁。
ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的。
而ConcurrentHashMap的读取并发才是真正体现其优势的地方,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(其实在锁粒度太多的时候也不是很好)。但是在求size等操作时也是需要锁定整个表。
ConcurrentHashMap使用了不同于传统集合的快速失败迭代器,使用的是弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
代码写一下迭代器,一会补充
也可以看一下 ConcurrentHashMap中的各种实现方式。
2)HashTable
后期补充,跳表。
2、队列型同步容器
1)CopyOnWriteList(写时复制)
CopyOnWriteList即写时复制数组,就像它的名字一样,CopyOnWriteList在容器发生变更时,基于当前容器复制出一个新的容器,然后在新容器里变更,最后将旧容器的引用指向新容器,通过与ReentrantLock搭配实现线程安全。而对于容器的读是直接读取旧的指向是无锁操作。
以add方法进行举例,摘抄部分源码。
//CopyOnWriteArrayList中两个成员变量,用来实现线程安全和防止指令重排
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制出新的数组进行操作
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//是不是感觉这步有点多余
//array 字段是被 volutile 修饰,所以调用 setArray() 方法会是缓存行内的 array 字段缓存失败,并防止指令重拍,即 happens-before 原理
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
正如上面源码可以看出,在进行修改操作时,会复制一个新的底层数组,所以在读的时候,直接读取原array数组就行,就不需要加锁。读取效率非常高。
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
//获取的是原数组
final Object[] getArray() {
return array;
}