一、传统集合的缺陷
传统的集合,在并发访问的时候,是有问题的。如hashset、hashmap和arrayList,多个线程在对他们取数据、放数据的时候,是有问题的,因为他们是线程不安全的。由于没有控制并发,会导致数据的不一致,引起死循环。
为什么会引起死循环?拿HashMap来看,看一下HashMap的get函数的源代码:
public V get(Object key){
if(key == null){ return getForNullKey(); }
int hash = hash(key.hashCode());
for(Entry<k,v> e = table[indexFor(hash,table.length)];
e != null;
e = e.next()){
if(e.hash == hash && ((k = e.key) == key||key.equals(k)))
return e.value;
}
return null;
}</k,v>
get函数会根据key的hashCode来锁定多个对象,并且遍历这些对象来找到key所对应的对象。当多个线程不安全的修改HashMap数据结构的时候,有可能使得这个函数进入死循环。
再比如一个线程在遍历数据
1 2 3 |
|
其中hasnext
1 2 3 4 5 6 |
|
此时还有一个线程在取数据
1 2 3 |
|
也就是说,一个循环取,一个循环拿的时候,取的集合的next已经到底了,而拿的集合打断了取的过程,从集合中拿了数据,改变了标志位count的值,这个时候取的集合继续运行,发现count++还没到底,继续循环,此时又有其他取的线程打断它,那么这个循环就会永远运行下去不跳出来,这就又形成了死循环。如果要使用到多个线程中去,需要加上自己的同步标志或者使用concurrent包下的同步集合。
传统方式下的Collection在迭代时,不允许对集合进行修改。使用Iterator对集合进行迭代时不能修改集合。
下面的代码在对集合修改时就会报异常.
public class CollectionModifyExceptionTest {
public static void main(String[] args) {
List<String> strs = new ArrayList<>();
strs.add("aaa");
strs.add("bbb");
strs.add("ccc");
Iterator iterator = strs.iterator();
while(iterator.hasNext()){
System.out.println(".....");
String value = (String)iterator.next();
if("aaa".equals(value)){
strs.remove(value);
}else{
System.out.println(value);
}
}
}
}
二、传统同步集合
要拿到一个传统同步集合很简单:,Collections.synchronizedMap(Map someMap);给它一个普通的Map,它就会返回一个线程安全的同步Map,线程同步的Map,其中的所有操作都是加了synchronized (mutex)约束的,保证了每一个操作都是互斥的、线程安全的。
源码如下:
private static class SynchronizedMap<k,v>
implements Map<k,v>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<k,v> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<k,v> m) {
if (m==null)
throw new NullPointerException();
this.m = m;
mutex = this;
}
SynchronizedMap(Map<k,v> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
public void putAll(Map<!--? extends K, ? extends V--> map) {
synchronized (mutex) {m.putAll(map);}
}
public void clear() {
synchronized (mutex) {m.clear();}
}
private transient Set<k> keySet = null;
private transient Set<map.entry<k,v>> entrySet = null;
private transient Collection<v> values = null;
public Set<k> keySet() {
synchronized (mutex) {
if (keySet==null)
keySet = new SynchronizedSet<>(m.keySet(), mutex);
return keySet;
}
}
public Set<map.entry<k,v>> entrySet() {
synchronized (mutex) {
if (entrySet==null)
entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
return entrySet;
}
}
public Collection<v> values() {
synchronized (mutex) {
if (values==null)
values = new SynchronizedCollection<>(m.values(), mutex);
return values;
}
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return m.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return m.hashCode();}
}
public String toString() {
synchronized (mutex) {return m.toString();}
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}
</v></map.entry<k,v></k></v></map.entry<k,v></k></k,v></k,v></k,v></k,v></k,v>
三、并发库提供的同步集合
同样,java5的并发库中也提供了相应的同步集合:Collection 实现:
ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet,CopyOnWriteArrayList,CopyOnWriteArraySet
ConcurrentHashMap:
HashTable是HashMap的线程安全实现,但是HashTable使用synchronized来保证线程安全,这就会导致它的效率非常低下,因为当线程1使用put添加元素,线程2不但不能使用put添加元素,同时也不能使用get获取元素,竞争越激烈效率越低。
因此替代HashTable的ConcurrentHashMap就出现了,ConcurrentHashMap的优点在于容器里有多把锁,每一把锁用于锁容器其中一部分数据,当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。它的原理是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。有些方法需要跨段,如size()和containsValue(),他们可能需要锁定整个表而不仅是某个段,这需要按顺序锁定所有段,操作完毕后又按顺序释放所有段的锁。
CopyOnWriteArrayList:
Copy-On-Write是一种用于程序设计中的优化策略,基本思路是多个线程共享同一个列表,当某个线程想要修改这个列表的元素时会把列表中的元素Copy一份,然后进行修改,修改完后再讲新的元素设置给这个列表,是一种延时懒惰策略。好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加、移除任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。使用Copy-On-Write机制实现的并发容器有两个分别是:CopyOnWriteArrayList和CopyOnWriteArraySet。
下面来分析下CopyOnWriteArrayList的核心源码,首先看下add方法:
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;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可以看到在添加的时候进行了加锁操作,否则多线程写的时候会Copy出N个副本出来。复制一份之后将新的元素设置到元素数组的len位置,然后再把最新的元素设置给该列表。
get方法:
public E get(int index) {
return get(getArray(), index);
}
读不需要加锁,如果读的时候多个线程正在向容器内添加数据,还是会读到旧数据,因为写的时候不会锁住旧的元素数组。这种写时拷贝的原理优点是读写分离,并发场景下操作效率会提高,缺点是写操作时占用的内存空间翻了一倍,因此是以空间换时间。
使用同步集合实现迭代时对集合进行修改:
package cn.itcast.heima;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CollectionModifyExceptionTest {
public static void main(String[] args) {
Collection users = new CopyOnWriteArrayList();//在写的时候有一分拷贝
users.add(new User("张三",28));
users.add(new User("李四",25));
users.add(new User("王五",31));
Iterator itrUsers = users.iterator();
while(itrUsers.hasNext()){
System.out.println("aaaa");
User user = (User)itrUsers.next();
if("李四".equals(user.getName())){
users.remove(user);
//itrUsers.remove();
} else {
System.out.println(user);
}
}
}
}