约束条件:
使用jdk1.7版本
问题背景:
从消息队列中取数据存放到一个公共区,数据消费者从公共区取数据消费数据。每次生产者生产的数据有可能是重复的,而要保证消费者每次一次性消费取到的数据时唯一的。
问题思路:
1、首先想到第一个解决方法是生产者生产的数据存放到公共区时做去重操作,而数据结构Set刚好满足唯一性这一特征,只要覆盖你自定义的YourDefineClass中hashCode()和equals()方法即可。所以考虑用Set作为公共区。
2、如何保证Set的线程安全?同步控制我想到用包装工具类
Set<YourDefineClass> set = Collections.synchronizedSet(new HashSet<YourDefineClass>());
来包装非线程安全的HashSet(),一切似乎很完美。
遇到问题:
在多线程环境中用foreach访问Set中元素时抛java.util.ConcurrentModificationException异常。
问题分析追踪:
使用foreach遍历元素,本质上上调用iterator()接口方法。从异常类型看似乎是同步时出现问题。先看看Collections.synchronizedSet(Set s)是如何包装Set的。
先上源代码:
public static <T> Set<T> synchronizedSet(Set<T> s) {
return new SynchronizedSet<>(s);
}
static class SynchronizedSet<E>
extends SynchronizedCollection<E>
implements Set<E> {
private static final long serialVersionUID = 487447009682186044L;
SynchronizedSet(Set<E> s) {
super(s);
}
SynchronizedSet(Set<E> s, Object mutex) {
super(s, mutex);
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return c.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return c.hashCode();}
}
}
从Collections工具类中的SynchronizedSet看,jdk使用互斥信号量mutex做同步控制,而且只是对Set中的equals和hashCode做同步控制。SynchronizedSet还继承了抽象类SynchronizedCollection。下面再看看SynchronizedCollection做了哪些事情。
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
if (c==null)
throw new NullPointerException();
this.c = c;
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = c;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
public String toString() {
synchronized (mutex) {return c.toString();}
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}
注意到iterator() 有没有什么特别的地方没,没错,方法体内竟然没有用到mutex做同步控制。也就是说当你使用Collections.synchronizedSet(new HashSet())包装非线程安全的的HashSet后,除了iterator()之外的其他方法都保证了线程安全。也就是使用该包装后的HashSet在多线程环境做foreach遍历时会不受互斥量控制,在边增删改元素同时访问Set中元素会抛出java.util.ConcurrentModificationException。不知jdk作者为啥只单对对iterator不做控制?
解决方法
最终我用java并发包中concurrent中的CopyOnWriteArraySet实现了同步控制
Set<YourDefineClass> set = new CopyOnWriteArraySet<YourDefineClass>();
当然还有好多种解决方案,未来用到的话继续补充。