线程不安全集合类及源码剖析:
常用的线程不安全集合:
ArrayList
LinkedList
ArraySet
HashMap
不安全集合之List
1.ArrayList
举一个List线程不安全的例子: 开10个线程对List进行添加并访问。
public static void main(String[] args) {
//新建一个ArrayList集合
List<String> strings = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
strings.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(Thread.currentThread().getName()+":"+strings);
},String.valueOf(i)).start();
}
}
我们会发现,后台会报出如下异常:Exception in thread "14" java.util.ConcurrentModificationException
(并发修改异常),当方法检测到对象的并发修改,但不允许这种修改时,就会抛出此异常。
我们来看ArrayList类 add方法的源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!(自动扩容)
elementData[size++] = e;
return true;
}
我们可以看到,ArrayList的底层维护了一个Obj类型的数组,它首先更新的数组的长度,然后将参数赋给指定的数组。其并没有加锁,因此是线程不安全的。
举个例子:
比如当A线程在操作数据时,B线程突然进来,将A线程要添加的位置占用,后续A线程将此数据(B线程的数据)修改为自己的数据这时就会抛出异常(java.util.ConcurrentModificationException)
2.LinkedList
我们来看LinkedList的add方法源码:
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
LinkedList 底层是基于双向链表实现的,也是实现了 List 接口,所以也拥有 List 的一些特点 ,可见每次插入都是移动指针而且没有加锁,因此LinkedList 为线程不安全集合。
3.解决办法
- 使用线程安全的Vector()类
List list = new Vector()
,Vector的add方法使用了synchronized锁。因为加了锁,所以导致效率低。
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
- 使用Collections的synchronizedList方法。
Collections.synchronizedList(new ArrayList<>());
它是集合框架的工具类,从方法名来看,就是创建一个线程安全的arraylist。
List<String> list = new CopyOnWriteArrayList<>();
// 写时复制
/**
* CopyOnWrite 容器即写时复制容器。往一个容器添加元素时,不直接往当前容器Object[] 添加,而是先将当前容器
* Object[] 进行Copy, 复制出一个新容器 Object[] newElements, 然后新的容器 Object[] newElements
* 里添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(newElements); 这样做的好处是可以对
* CopyOnWrite 容器进行并发读, 而不需要加锁,因为当前容器不会添加任何元素。 所以CopyOnWrite 容器也是一
* 种读写分离的思想,读和写不同容器。
*/
// 源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try{
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyof(element, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
- 使用JUC包下的CopyOnWriteArrayList,它也是大厂经常使用的并发类之一。
CopyOnWrite 容器即写时复制容器。往一个容器添加元素时,不直接往当前容器Object[] 添加,而是先将当前容器Object[] 进行Copy, 复制出一个新容器 Object[] newElements, 然后新的容器 Object[] newElements
里添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(newElements); 这样做的好处是可以对CopyOnWrite 容器进行并发读, 而不需要加锁,因为当前容器不会添加任何元素。 所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同容器。
不安全线程之Set,Map
Set<String> set = new HashSet<>();
//HashSet()底层用的是HashMap
//源码:
public HashSet() {
map = new HashMap<>();
}
我们将HashSet HashMap 做上面同样的实验会发现,都会抛出并发修改异常。
解决方案:
Set集合的解决方案:
- Collections.synchronizedSet(new HashSet()) ;
- new CopyOnWriteArraySet();
Map集合的解决方案:
- 使用HashTable,源码如下,我们发现其put方法使用了synchronized锁。
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
- 使用ConcurrentHashMap(线程安全)
Map<String,String> maps = new ConcurrentHashMap<String, String>()